Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Preface

This documentation covers mxcli, a command-line tool for reading, querying, and modifying Mendix application projects, and MDL (Mendix Definition Language), the SQL-like language it uses. The material is organized into eight parts:

  • Part I: Tutorial walks you through installation, exploring a project, making your first changes, and integrating with AI assistants.
  • Part II: The MDL Language is a comprehensive guide to MDL syntax – domain models, microflows, pages, security, navigation, workflows, and business events.
  • Part III: Project Tools covers code navigation, catalog queries, linting, testing, external SQL, and Docker integration.
  • Part IV: IDE Integration describes the VS Code extension, LSP server, and mxcli init project setup.
  • Part V: Go Library documents the Go API for programmatic access to Mendix projects.
  • Part VI: MDL Statement Reference provides detailed syntax and examples for every MDL statement.
  • Part VII: Architecture and Internals explains the MPR file format, parser pipeline, and catalog system for contributors.
  • Part VIII: Appendixes includes quick reference tables, reserved words, error messages, and a glossary.

You can read the Tutorial sequentially or jump directly to the section relevant to your task. The Statement Reference (Part VI) is designed for lookup rather than linear reading.

What is mxcli?

mxcli is a command-line tool that enables developers and AI coding assistants to read, understand, and modify Mendix application projects. Mendix projects are stored in binary .mpr files that cannot be read or edited as text. mxcli bridges this gap by providing a text-based interface using MDL (Mendix Definition Language), a SQL-like syntax for querying and manipulating Mendix models.

How It Works

┌─────────────────┐     ┌──────────────┐     ┌─────────────────┐
│   Developer     │     │    mxcli     │     │ Mendix Project  │
│   or AI Agent   │────>│  (MDL REPL)  │────>│   (.mpr file)   │
│                 │ MDL │              │     │                 │
│ "Create a       │     │ Parses MDL   │     │ Creates actual  │
│  Customer       │     │ Validates    │     │ entities,       │
│  entity..."     │     │ Executes     │     │ microflows,     │
└─────────────────┘     └──────────────┘     │ pages, etc.     │
                                             └─────────────────┘

A developer or AI agent writes MDL statements. mxcli parses and validates them, then applies the changes directly to the .mpr project file. The modified project can then be opened in Mendix Studio Pro as usual.

Important: Do not edit a project with mxcli while it is open in Studio Pro. Studio Pro maintains in-memory caches that cannot be updated externally, and concurrent edits will cause errors. Close the project in Studio Pro first, run mxcli, then re-open the project.

Key Capabilities

  • Project exploration – list modules, entities, microflows, pages; describe any element in MDL; full-text search across all strings and source definitions.
  • Code navigation – find callers, callees, references, and impact analysis for any project element.
  • Model modification – create and alter entities, microflows, pages, security rules, navigation, workflows, and more using MDL scripts.
  • Catalog queries – SQL-based querying of project metadata (entity counts, microflow complexity, widget usage, cross-references).
  • Linting and reports – 40+ built-in lint rules with SARIF output for CI; scored best-practices reports.
  • Testing – test microflows using MDL syntax with javadoc-style annotations.
  • AI assistant integration – works with Claude Code, Cursor, Continue.dev, Windsurf, and Aider. mxcli init sets up project configuration, skills, and a Dev Container for sandboxed AI development.
  • VS Code extension – syntax highlighting, diagnostics, code completion, hover, go-to-definition, and context menu commands for .mdl files.
  • External SQL – connect to PostgreSQL, Oracle, or SQL Server; query external databases; import data into a running Mendix application.

Supported Environments

ItemDetails
Mendix versionsStudio Pro 8.x, 9.x, 10.x, 11.x
MPR formatsv1 (single .mpr file) and v2 (.mpr + mprcontents/ folder)
PlatformsLinux, macOS, Windows (amd64 and arm64)
DependenciesNone – mxcli is a single static binary with no runtime dependencies

What is MDL?

MDL (Mendix Definition Language) is a text-based, SQL-like language for describing and manipulating Mendix application models. It provides a human-readable representation of Mendix project elements – entities, microflows, pages, security rules, and more – at the same abstraction level as the visual models in Studio Pro.

Why a Text-Based Language?

Mendix projects are stored in a binary format (.mpr files containing BSON documents in SQLite). While Studio Pro provides a visual interface, there is no standard text format for Mendix models. This creates problems for:

  • AI assistants that need to read and generate model elements
  • Code review of changes to Mendix projects
  • Scripting and automation of repetitive modeling tasks
  • Documentation of project structure and design patterns

MDL addresses these problems by providing a syntax that is both human-readable and machine-parseable.

Characteristics of MDL

  • SQL-like syntax – uses familiar keywords like CREATE, ALTER, DROP, SHOW, DESCRIBE, GRANT.
  • Declarative – you describe what the model should look like, not how to construct it step by step.
  • Complete – covers domain models, microflows, pages, security, navigation, workflows, and business events.
  • Validated – mxcli can check MDL scripts for syntax errors and reference validity before applying them.

Token Efficiency

When working with AI coding assistants, context window size is a critical constraint. MDL’s compact syntax uses significantly fewer tokens than equivalent JSON model representations:

RepresentationTokens for a 10-entity module
JSON (raw model)~15,000–25,000 tokens
MDL~2,000–4,000 tokens
Reduction5–10x fewer tokens

Fewer tokens means lower API costs, more application context fits in a single prompt, and AI assistants produce more accurate output because there is less noise in their input.

Example: JSON vs MDL

A simple entity definition illustrates the difference.

JSON representation (~180 tokens):

{
  "entity": {
    "name": "Customer",
    "documentation": "Customer master data",
    "attributes": [
      {"name": "Name", "type": {"type": "String", "length": 200}},
      {"name": "Email", "type": {"type": "String", "length": 200}}
    ]
  }
}

MDL representation (~35 tokens):

/** Customer master data */
CREATE PERSISTENT ENTITY Sales.Customer (
    Name: String(200),
    Email: String(200)
);

The MDL version conveys the same information in roughly one-fifth the space, and its structure is immediately recognizable to anyone familiar with SQL DDL.

What MDL Covers

MDL provides statements for the following areas of a Mendix project:

AreaExample Statements
Domain modelCREATE ENTITY, ALTER ENTITY, CREATE ASSOCIATION
MicroflowsCREATE MICROFLOW, CREATE NANOFLOW
PagesCREATE PAGE, CREATE SNIPPET, ALTER PAGE
SecurityCREATE MODULE ROLE, GRANT, REVOKE
NavigationALTER NAVIGATION
WorkflowsCREATE WORKFLOW
Business eventsCREATE BUSINESS EVENT SERVICE
Project queriesSHOW MODULES, DESCRIBE ENTITY, SEARCH
CatalogREFRESH CATALOG, SELECT FROM CATALOG

See Part II (The MDL Language) for a complete guide, or Part VI (MDL Statement Reference) for detailed syntax of each statement.

Mendix Concepts for Newcomers

This page explains the core Mendix concepts you’ll encounter when using mxcli. If you’re familiar with web frameworks, relational databases, or backend development, most of these will map to things you already know.

The Big Picture

A Mendix application is a model-driven app. Instead of writing code in files, developers build applications visually in Studio Pro (the Mendix IDE). The entire application model – data structures, logic, UI, security – is stored in a single binary file called an MPR (Mendix Project Resource).

mxcli lets you read and modify that MPR file using text commands, without opening Studio Pro.

Modules

A module is the top-level organizational unit, like a package in Go or a namespace in C#. Each module has its own:

  • Domain model (data structures)
  • Microflows and nanoflows (logic)
  • Pages (UI)
  • Enumerations (constant sets)
  • Security settings

A typical project has a few custom modules (Sales, Admin, Integration) plus system modules provided by the platform (System, Administration).

SHOW MODULES;

Domain Model

The domain model is the data layer of a module. If you know relational databases, the mapping is straightforward:

Mendix ConceptRelational EquivalentMDL Syntax
EntityTableCREATE ENTITY
AttributeColumnInside entity definition
AssociationForeign key / Join tableCREATE ASSOCIATION
GeneralizationTable inheritanceEXTENDS
EnumerationEnum / Check constraintCREATE ENUMERATION

Entities

An entity defines a data type. There are several kinds:

  • Persistent – stored in the database (the default and most common)
  • Non-persistent – exists only in memory during a user session, useful for form state or temporary calculations
  • View – backed by an OQL query, like a database view
CREATE PERSISTENT ENTITY Sales.Customer (
    Name: String(200) NOT NULL,
    Email: String(200),
    IsActive: Boolean DEFAULT true
);

Associations

An association is a relationship between two entities. Think of it as a foreign key.

  • Reference – many-to-one or one-to-one (a foreign key column on the “from” entity)
  • ReferenceSet – many-to-many (a join table under the hood)
CREATE ASSOCIATION Sales.Order_Customer
    FROM Sales.Order TO Sales.Customer
    TYPE Reference;

Generalization

Entities can inherit from other entities using generalization (the EXTENDS keyword). The child entity gets all parent attributes. This is commonly used with system entities like System.Image (for file uploads) or System.User (for user accounts).

CREATE PERSISTENT ENTITY Sales.ProductImage EXTENDS System.Image (
    Caption: String(200)
);

Microflows and Nanoflows

Microflows are the server-side logic of a Mendix app. They’re visual flowcharts in Studio Pro, but in MDL they read like imperative code with activities:

  • Retrieve data from the database
  • Create, Change, Commit, Delete objects
  • Call other microflows or external services
  • If/Else branching, Loop iteration
  • Show Page, Log Message, Validation Feedback
CREATE MICROFLOW Sales.CreateOrder(
    DECLARE $Customer: Sales.Customer
)
RETURN Boolean
BEGIN
    CREATE $Order: Sales.Order (
        OrderDate = [%CurrentDateTime%],
        Status = 'Draft'
    );
    CHANGE $Order (
        Sales.Order_Customer = $Customer
    );
    COMMIT $Order;
    RETURN true;
END;

Nanoflows are the client-side equivalent. They run in the browser (or native mobile app) and are useful for offline-capable logic and low-latency UI interactions. They have the same syntax but fewer available activities (no database transactions, no direct service calls).

Pages

Pages define the user interface. A page has:

  • A layout – the outer frame (header, sidebar, footer) shared across pages
  • Widgets – the UI components inside the page

Widgets are nested in a tree. Common widget types:

WidgetPurposeAnalogy
DataViewDisplays one objectA form
DataGridDisplays a list as a tableAn HTML table with sorting/search
ListViewDisplays a list with custom layoutA repeating template
TextBoxText input bound to an attributeAn <input> field
ButtonTriggers an actionA <button>
ContainerGroups other widgetsA <div>
LayoutGridResponsive column layoutCSS grid / Bootstrap row
CREATE PAGE Sales.CustomerOverview
    LAYOUT Atlas_Core.Atlas_Default
    TITLE 'Customers'
(
    DATAGRID SOURCE DATABASE Sales.Customer (
        COLUMN Name,
        COLUMN Email,
        COLUMN IsActive
    )
);

Snippets

A snippet is a reusable page fragment. You define it once and embed it in multiple pages using a SnippetCall. Think of it as a component or partial template.

Security

Mendix uses a role-based access control model:

  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 profiles define how users move through the app. There are profiles for responsive web, tablet, phone, and native mobile. Each profile has:

  • A home page (the landing page after login)
  • A menu with items that link to pages or microflows

Workflows

Workflows model long-running processes with human tasks. Think of them as state machines for approval flows, onboarding processes, or multi-step procedures. A workflow has:

  • User tasks – steps that require human action
  • Decisions – branching based on conditions
  • Parallel splits – concurrent paths

Workflows complement microflows: microflows handle immediate logic, workflows handle processes that span hours or days.

How It All Fits Together

Project
├── Module: Sales
│   ├── Domain Model
│   │   ├── Entity: Customer (Name, Email, IsActive)
│   │   ├── Entity: Order (OrderDate, Status)
│   │   └── Association: Order_Customer
│   ├── Microflows
│   │   ├── CreateOrder
│   │   └── ApproveOrder
│   ├── Pages
│   │   ├── CustomerOverview
│   │   └── OrderEdit
│   ├── Enumerations
│   │   └── OrderStatus (Draft, Active, Closed)
│   └── Security
│       ├── Module Role: Manager
│       └── Module Role: Viewer
├── Module: Administration
│   └── ...
└── Navigation
    └── Responsive profile → Home: Sales.CustomerOverview

In mxcli, you can explore this structure with:

SHOW STRUCTURE DEPTH 2;

What’s Next

Document Conventions

This page describes the formatting and notation conventions used throughout this documentation.

Code Examples

MDL examples use sql code fencing because MDL’s syntax is SQL-like and this provides appropriate syntax highlighting:

CREATE PERSISTENT ENTITY MyModule.Customer (
    Name: String(200) NOT NULL
);

Go code examples use go code fencing:

reader, err := modelsdk.Open("/path/to/app.mpr")
defer reader.Close()

Shell commands use bash code fencing:

mxcli -p app.mpr -c "SHOW MODULES"

Syntax Notation

When describing the syntax of MDL statements, the following conventions apply:

NotationMeaning
UPPERCASEKeywords – type them exactly as shown
lowercaseUser-provided values (names, expressions, types)
[brackets]Optional clause – may be omitted
...Repetition – the preceding element may appear multiple times
`ab`
( )Grouping – used to clarify precedence in syntax descriptions

For example, the notation:

CREATE [PERSISTENT] ENTITY module.name (
    attribute_name: type [NOT NULL] [, ...]
);

means: CREATE and ENTITY are required keywords; PERSISTENT is optional; module.name is a user-provided qualified name; each attribute has a name and type, with an optional NOT NULL constraint; and additional attributes may follow, separated by commas.

Cross-References

References to MDL statements link to their detailed pages in Part VI (MDL Statement Reference) using the format “See CREATE ENTITY” or “See GRANT”. References to conceptual explanations link to the relevant section in Part II (The MDL Language).

Terminology

Mendix-specific terms such as “entity”, “microflow”, “nanoflow”, “module”, and “domain model” follow standard Mendix terminology. See the Glossary for definitions.

Setting Up

Before you can start exploring and modifying Mendix projects from the command line, you need three things:

  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

There are four ways to get started with mxcli:

  • Playground – open the mxcli Playground in a GitHub Codespace. Zero install, runs in your browser with a sample Mendix project, tutorials, and example scripts. Best way to try mxcli for the first time.
  • Binary download – grab a pre-built binary from GitHub Releases. Quickest path if you want to use mxcli on your own project.
  • Build from source – clone the repo and run make build. Useful if you want the latest unreleased changes or plan to contribute.
  • Dev Container (recommended for your own projects) – run mxcli 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

Pick whichever method suits your situation. If you just want to try mxcli without installing anything, start with the Playground. If you’re planning to use mxcli on your own project with an AI coding assistant, skip to the Dev Container section.

Playground (zero install)

The fastest way to try mxcli. The mxcli Playground is a GitHub repository with a pre-configured Mendix project, example scripts, and tutorials. Open it in a Codespace and start using mxcli immediately – nothing to install on your machine.

Open in GitHub Codespaces

The Codespace comes with mxcli, a JDK, Docker-in-Docker, Claude Code, and a sample Mendix 11.x project ready to explore and modify. It includes:

  • 5 example scripts – explore, create entities, microflows, pages, and security
  • Step-by-step tutorials – from first steps through linting and testing
  • AI tool configs – pre-configured for Claude Code, Cursor, Windsurf, Continue.dev, and Aider

Once the Codespace is running:

./mxcli -p App.mpr -c "SHOW STRUCTURE"          # Explore the project
./mxcli exec scripts/01-explore.mdl -p App.mpr   # Run an example script
./mxcli                                           # Start interactive REPL

When you’re ready to work on your own Mendix project, use one of the installation methods below.

Binary download

Pre-built binaries are available for Linux, macOS, and Windows on both amd64 and arm64 architectures.

  1. Go to the GitHub Releases page.
  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

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

The Dev Container approach is the recommended way to work with mxcli, particularly when using AI coding assistants. It creates a sandboxed environment so that agents can only access your project files, not the rest of your system.

Here’s how to set it up:

Step 1: Install mxcli using one of the methods above (you need it locally to run init).

Step 2: Run mxcli init on your Mendix project:

mxcli init /path/to/my-mendix-project

This creates a .devcontainer/ folder (along with skill files, agent configs, and other goodies) inside your project directory.

Step 3: Open the project folder in VS Code and click “Reopen in Container” when prompted (or use the Command Palette: Dev Containers: Reopen in Container).

VS Code will build and start the container. This takes a minute or two the first time.

What’s inside the Dev Container

The container comes with everything you need pre-installed:

ComponentWhat it’s for
mxcliThe CLI itself, copied into the project
JDK 21 (Adoptium)Required by MxBuild for project validation
Docker-in-DockerRunning Mendix apps locally with mxcli docker run
Node.jsPlaywright testing support
PostgreSQL clientDatabase connectivity for demo data
Claude CodeAI coding assistant (auto-installed on container creation)

Once the container is running, mxcli is ready to use – no further setup needed.

Specifying AI tools

By default, mxcli init configures for Claude Code. You can target other tools too:

# Cursor only
mxcli init --tool cursor /path/to/my-mendix-project

# Multiple tools
mxcli init --tool claude --tool cursor /path/to/my-mendix-project

# Everything
mxcli init --all-tools /path/to/my-mendix-project

Run mxcli init --list-tools to see all supported tools.

Verify your installation

Whichever method you used, confirm that mxcli is working:

mxcli --version

You should see version and build information printed to the terminal. If you get a “command not found” error, double-check that the binary is on your PATH.

You’re ready to open a project.

Opening Your First Project

mxcli works with Mendix project files – the .mpr files that Mendix Studio Pro creates. Let’s open one.

Back up first. mxcli is alpha software and can corrupt project files. Before you start, either make a copy of your .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 most common way to point mxcli at a project is with the -p flag:

mxcli -p /path/to/app.mpr -c "SHOW MODULES"

This opens the project in read-only mode, runs the command, and exits. The -p flag works with all mxcli subcommands.

Opening in the REPL

You can also open a project from inside the interactive REPL:

# Start the REPL, then open a project
mxcli
OPEN PROJECT '/path/to/app.mpr';
SHOW MODULES;

Or pass -p when launching the REPL so it opens the project immediately:

mxcli -p /path/to/app.mpr
-- Project is already open, start working
SHOW MODULES;

When you provide -p, the project stays open for the duration of your REPL session. You don’t need to pass it again for each command.

Read-only vs read-write

By default, mxcli opens projects in read-only mode. This is safe for exploration – you can browse modules, describe entities, search through the project, and run catalog queries without risk.

When you execute a command that modifies the project (like CREATE ENTITY), mxcli automatically upgrades to read-write mode. You’ll see a confirmation message when this happens.

MPR format auto-detection

Mendix projects come in two formats:

  • v1: A single .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

If you’re using the Dev Container setup from the previous page, your project is already mounted in the container. The typical path is:

mxcli -p app.mpr

The Dev Container sets the working directory to your project root, so relative paths work naturally.

Quick sanity check

Once you have a project open, try listing the modules:

mxcli -p app.mpr -c "SHOW MODULES"

You should see a table of module names. If you see your project’s modules listed, everything is working and you’re ready to explore.

Next up: the REPL, where the real fun starts.

The REPL

The mxcli REPL is an interactive shell for working with Mendix projects, much like psql is for PostgreSQL or mysql for MySQL. You type MDL statements, press Enter, and see results immediately.

Starting the REPL

Launch it with or without a project:

# Start with a project already loaded
mxcli -p app.mpr

# Start without a project (you can open one later)
mxcli

You’ll see a prompt where you can start typing MDL:

mxcli>

Running commands

Type any MDL statement and press Enter:

SHOW MODULES;

Results are printed as formatted tables directly in the terminal.

Multi-line statements

MDL statements end with a semicolon (;). If you press Enter before typing a semicolon, mxcli knows you’re still writing and waits for more input:

CREATE ENTITY MyModule.Customer (
    Name: String(200) NOT NULL,
    Email: String(200),
    IsActive: Boolean DEFAULT true
);

The REPL shows a continuation prompt (.....>) while you’re in a multi-line statement. The statement executes when you type the closing ; and press Enter.

Getting help

Type HELP to see a list of available commands:

HELP;

This prints a categorized overview of all MDL statements – useful when you can’t remember the exact syntax for something.

Command history

Use the up and down arrow keys to scroll through previous commands, just like in any other shell. This is especially handy when you’re iterating on a query or tweaking a CREATE statement.

Exiting

When you’re done, type either:

EXIT;

or:

QUIT;

You can also press Ctrl+D to exit.

One-off commands with -c

You don’t always need an interactive session. The -c flag lets you run a single command and exit:

mxcli -p app.mpr -c "SHOW ENTITIES IN MyModule"

This is great for quick lookups and for scripting. The output is pipe-friendly, so you can chain it with other tools:

# Count entities per module
mxcli -p app.mpr -c "SHOW MODULES" | tail -n +2 | while read module; do
    echo "$module: $(mxcli -p app.mpr -c "SHOW ENTITIES IN $module" | wc -l) entities"
done

Running script files

For anything beyond a quick one-liner, you can put MDL statements in a .mdl file and execute the whole thing:

mxcli -p app.mpr -c "EXECUTE SCRIPT 'setup.mdl'"

Or from the REPL:

EXECUTE SCRIPT 'setup.mdl';

This is how you’ll typically apply larger changes – write the MDL in a file, check the syntax with mxcli check, then execute it.

The TUI (Terminal UI)

If you prefer a more visual experience, mxcli also offers a graphical terminal interface:

mxcli tui -p app.mpr

The TUI gives you a split-pane layout with a project tree, an MDL editor, and output panels. It’s built on top of the same REPL engine, so all the same commands work. This can be a nice middle ground between the bare REPL and opening Studio Pro.

What’s next

Now that you know how to install mxcli, open a project, and use the REPL, you’re ready to start exploring. The next chapter walks through the commands you’ll use most often: SHOW, DESCRIBE, and SEARCH.

Exploring a Project

Once you have a project open – whether through the REPL, a CLI one-liner, or a script file – the next step is to look around. What modules exist? What entities are defined? What does a particular microflow do?

mxcli provides three families of commands for exploration:

  • SHOW commands list elements by type. SHOW 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

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

You should have mxcli installed and know how to open a project. If not, work through the Setting Up chapter first.

The examples in this chapter assume you have a Mendix project open. You can follow along using either the REPL or CLI one-liners:

# REPL (interactive)
mxcli -p /path/to/app.mpr

# CLI one-liner
mxcli -p /path/to/app.mpr -c "SHOW ENTITIES"

If you do not have a Mendix project handy, the commands will still make sense – the output format and options are the same regardless of the project content.

SHOW MODULES, SHOW ENTITIES

The SHOW family of commands lists project elements by type. They are the fastest way to see what a project contains.

Listing modules

Every Mendix project is organized into modules. Start by listing them:

SHOW MODULES;

Example output:

MyFirstModule
Administration
Atlas_Core
System

By default, system and marketplace modules (like System and Atlas_Core) are included. The modules appear in the order they are defined in the project.

Listing entities

To see all entities across all modules:

SHOW ENTITIES;

Example output:

Administration.Account
MyFirstModule.Customer
MyFirstModule.Order
MyFirstModule.OrderLine

Each entity is shown as a qualified name – the module name and entity name separated by a dot.

Filtering by module

Most SHOW commands accept an IN clause to filter results to a single module:

SHOW ENTITIES IN MyFirstModule;
MyFirstModule.Customer
MyFirstModule.Order
MyFirstModule.OrderLine

This is typically what you want when working on a specific module.

Listing microflows

SHOW MICROFLOWS;
Administration.ChangeMyPassword
MyFirstModule.ACT_Customer_Save
MyFirstModule.ACT_Order_Process
MyFirstModule.DS_Customer_GetAll

Filter to a module:

SHOW MICROFLOWS IN MyFirstModule;
MyFirstModule.ACT_Customer_Save
MyFirstModule.ACT_Order_Process
MyFirstModule.DS_Customer_GetAll

Listing pages

SHOW PAGES;
Administration.Account_Overview
Administration.Login
MyFirstModule.Customer_Overview
MyFirstModule.Customer_Edit
MyFirstModule.Order_Detail

Filter to a module:

SHOW PAGES IN MyFirstModule;
MyFirstModule.Customer_Overview
MyFirstModule.Customer_Edit
MyFirstModule.Order_Detail

Other SHOW commands

The same pattern works for all major element types:

SHOW ENUMERATIONS;
SHOW ENUMERATIONS IN MyFirstModule;

SHOW ASSOCIATIONS;
SHOW ASSOCIATIONS IN MyFirstModule;

SHOW WORKFLOWS;
SHOW WORKFLOWS IN MyFirstModule;

SHOW NANOFLOWS;
SHOW NANOFLOWS IN MyFirstModule;

SHOW CONSTANTS;
SHOW CONSTANTS IN MyFirstModule;

SHOW SNIPPETS;
SHOW SNIPPETS IN MyFirstModule;

You can also list security-related elements:

SHOW MODULE ROLES;
SHOW MODULE ROLES IN MyFirstModule;

SHOW USER ROLES;

SHOW DEMO USERS;

And navigation:

SHOW NAVIGATION;

Using SHOW from the command line

Every SHOW command works as a CLI one-liner with -c:

mxcli -p app.mpr -c "SHOW ENTITIES"
mxcli -p app.mpr -c "SHOW MICROFLOWS IN MyFirstModule"
mxcli -p app.mpr -c "SHOW PAGES"

This is useful for quick lookups without entering the REPL, and for piping output to other tools:

# Count entities per module
mxcli -p app.mpr -c "SHOW ENTITIES" | cut -d. -f1 | sort | uniq -c

# Find all microflows with "Save" in the name
mxcli -p app.mpr -c "SHOW MICROFLOWS" | grep -i save

Summary of SHOW commands

CommandDescription
SHOW MODULESList 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 ROLESList user roles
SHOW DEMO USERSList demo users
SHOW NAVIGATIONShow navigation profiles

Now that you can list elements, the next step is inspecting individual elements in detail with DESCRIBE and SEARCH.

DESCRIBE, SEARCH

While SHOW commands list elements by name, DESCRIBE gives you the full definition of a single element. SEARCH lets you find elements by keyword when you do not know the exact name.

DESCRIBE ENTITY

To see the complete definition of an entity, including its attributes, types, and constraints:

DESCRIBE ENTITY MyFirstModule.Customer;

Example output:

CREATE PERSISTENT ENTITY MyFirstModule.Customer (
  Name: String(200),
  Email: String(200),
  Phone: String(50),
  DateOfBirth: DateTime,
  IsActive: Boolean DEFAULT false
);

The output is valid MDL – you could copy it, modify it, and execute it as a CREATE OR MODIFY statement. This round-trip capability is one of the key design features of MDL.

DESCRIBE also shows associations and access rules when they exist:

DESCRIBE ENTITY MyFirstModule.Order;
CREATE PERSISTENT ENTITY MyFirstModule.Order (
  OrderNumber: AutoNumber,
  OrderDate: DateTime,
  TotalAmount: Decimal,
  Status: MyFirstModule.OrderStatus
);

CREATE ASSOCIATION MyFirstModule.Order_Customer
  FROM MyFirstModule.Order TO MyFirstModule.Customer
  TYPE Reference
  OWNER Default
  DELETE_BEHAVIOR DeleteRefSetOnly;

DESCRIBE MICROFLOW

To see the full logic of a microflow:

DESCRIBE MICROFLOW MyFirstModule.ACT_Customer_Save;

Example output:

CREATE MICROFLOW MyFirstModule.ACT_Customer_Save
  ($Customer: MyFirstModule.Customer)
  RETURNS Boolean
BEGIN
  IF $Customer/Name = empty THEN
    VALIDATION FEEDBACK $Customer/Name MESSAGE 'Name is required';
    RETURN false;
  END IF;

  COMMIT $Customer;
  RETURN true;
END;

This shows the parameters, return type, and the complete flow logic. For complex microflows, this can be quite long – but it gives you the full picture in one command.

DESCRIBE PAGE

Pages are shown as their widget tree:

DESCRIBE PAGE MyFirstModule.Customer_Edit;

Example output:

CREATE PAGE MyFirstModule.Customer_Edit
(
  Params: { $Customer: MyFirstModule.Customer },
  Title: 'Edit Customer',
  Layout: Atlas_Core.PopupLayout
)
{
  DATAVIEW dvCustomer (DataSource: $Customer) {
    TEXTBOX txtName (Label: 'Name', Attribute: Name)
    TEXTBOX txtEmail (Label: 'Email', Attribute: Email)
    TEXTBOX txtPhone (Label: 'Phone', Attribute: Phone)

    FOOTER footer1 {
      ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)
      ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES)
    }
  }
};

DESCRIBE ENUMERATION

DESCRIBE ENUMERATION MyFirstModule.OrderStatus;
CREATE ENUMERATION MyFirstModule.OrderStatus (
  Draft 'Draft',
  Submitted 'Submitted',
  Approved 'Approved',
  Rejected 'Rejected',
  Completed 'Completed'
);

Each value is followed by its caption (the display label shown to end users).

DESCRIBE ASSOCIATION

DESCRIBE ASSOCIATION MyFirstModule.Order_Customer;
CREATE ASSOCIATION MyFirstModule.Order_Customer
  FROM MyFirstModule.Order TO MyFirstModule.Customer
  TYPE Reference
  OWNER Default
  DELETE_BEHAVIOR DeleteRefSetOnly;

Other DESCRIBE targets

The same pattern works for other element types:

DESCRIBE WORKFLOW MyFirstModule.ApprovalFlow;
DESCRIBE NANOFLOW MyFirstModule.NAV_GoToDetail;
DESCRIBE SNIPPET MyFirstModule.CustomerCard;
DESCRIBE CONSTANT MyFirstModule.ApiBaseUrl;
DESCRIBE NAVIGATION;
DESCRIBE SETTINGS;

When you do not know the exact name of an element, use SEARCH to find it by keyword. This searches across all strings in the project – entity names, attribute names, microflow logic, page captions, messages, documentation, and more.

SEARCH 'validation';

Example output:

MyFirstModule.ACT_Customer_Save (Microflow)
  "VALIDATION FEEDBACK $Customer/Name MESSAGE 'Name is required'"

MyFirstModule.ACT_Order_Validate (Microflow)
  "VALIDATION FEEDBACK $Order/OrderDate MESSAGE 'Order date cannot be in the past'"

MyFirstModule.Customer_Edit (Page)
  "Validation errors will appear here"

Search is case-insensitive and matches partial words.

Search from the command line

The CLI provides a dedicated search subcommand with formatting options:

# Search with default output
mxcli search -p app.mpr "validation"

# Show only element names (no context)
mxcli search -p app.mpr "validation" --format names

# JSON output for programmatic use
mxcli search -p app.mpr "validation" --format json

The --format names option is useful for piping into other commands:

# Find all microflows mentioning "email" and describe each one
mxcli search -p app.mpr "email" --format names | while read name; do
  mxcli -p app.mpr -c "DESCRIBE MICROFLOW $name"
done

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

The SHOW STRUCTURE command gives you a compact, tree-style overview of a project. It is the fastest way to understand the overall shape of an application – what modules exist, what types of documents they contain, and how large each module is.

Default view

With no arguments, SHOW STRUCTURE displays all user modules at depth 2 – modules with their documents listed by type:

SHOW STRUCTURE;

Example output:

MyFirstModule
  Entities
    Customer (Name, Email, Phone, DateOfBirth, IsActive)
    Order (OrderNumber, OrderDate, TotalAmount, Status)
    OrderLine (Quantity, UnitPrice, LineTotal)
  Microflows
    ACT_Customer_Save ($Customer: Customer) : Boolean
    ACT_Order_Process ($Order: Order) : Boolean
    DS_Customer_GetAll () : List of Customer
  Pages
    Customer_Overview
    Customer_Edit ($Customer: Customer)
    Order_Detail ($Order: Order)
  Enumerations
    OrderStatus (Draft, Submitted, Approved, Rejected, Completed)
Administration
  Entities
    Account (FullName, Email, IsLocalUser, BlockedSince)
  Microflows
    ChangeMyPassword ($OldPassword, $NewPassword) : Boolean
  Pages
    Account_Overview
    Login

This gives you a bird’s-eye view without opening individual elements. Entity signatures show attribute names; microflow signatures show parameters and return types.

Depth levels

The DEPTH option controls how much detail is shown:

DEPTH 1 – Module summary

Shows one line per module with element counts:

SHOW STRUCTURE DEPTH 1;
MyFirstModule       3 entities, 3 microflows, 3 pages, 1 enumeration, 2 associations
Administration      1 entity, 1 microflow, 2 pages

This is useful for getting a quick sense of project size and where the complexity lives.

DEPTH 2 – Elements with signatures (default)

This is the default when you run SHOW STRUCTURE with no depth specified. It shows modules, their documents grouped by type, and compact signatures for each element. See the example under Default view above.

DEPTH 3 – Full detail

Shows typed attributes and named parameters:

SHOW STRUCTURE DEPTH 3;
MyFirstModule
  Entities
    Customer
      Name: String(200)
      Email: String(200)
      Phone: String(50)
      DateOfBirth: DateTime
      IsActive: Boolean DEFAULT false
    Order
      OrderNumber: AutoNumber
      OrderDate: DateTime
      TotalAmount: Decimal
      Status: MyFirstModule.OrderStatus
    OrderLine
      Quantity: Integer
      UnitPrice: Decimal
      LineTotal: Decimal
  Microflows
    ACT_Customer_Save ($Customer: MyFirstModule.Customer) : Boolean
    ACT_Order_Process ($Order: MyFirstModule.Order) : Boolean
    DS_Customer_GetAll () : List of MyFirstModule.Customer
  Pages
    Customer_Overview
    Customer_Edit ($Customer: MyFirstModule.Customer)
    Order_Detail ($Order: MyFirstModule.Order)
  Enumerations
    OrderStatus
      Draft 'Draft'
      Submitted 'Submitted'
      Approved 'Approved'
      Rejected 'Rejected'
      Completed 'Completed'
  Associations
    Order_Customer: Order -> Customer (Reference)
    OrderLine_Order: OrderLine -> Order (Reference)

Depth 3 is verbose but gives you the most complete picture without running individual DESCRIBE commands.

Filtering by module

Use IN to show only a single module:

SHOW STRUCTURE IN MyFirstModule;

This produces the same tree format but limited to one module. Combine with DEPTH for control over detail:

SHOW STRUCTURE DEPTH 3 IN MyFirstModule;

Including system modules

By default, system and marketplace modules are hidden. Add ALL to include them:

SHOW STRUCTURE DEPTH 1 ALL;
MyFirstModule       3 entities, 3 microflows, 3 pages, 1 enumeration, 2 associations
Administration      1 entity, 1 microflow, 2 pages
Atlas_Core          0 entities, 0 microflows, 12 pages
System              15 entities, 0 microflows, 0 pages

This is useful when you need to see system entities (like System.Image or System.FileDocument) or check what a marketplace module provides.

Using SHOW STRUCTURE from the command line

# Quick project overview
mxcli -p app.mpr -c "SHOW STRUCTURE DEPTH 1"

# Detailed view of one module
mxcli -p app.mpr -c "SHOW STRUCTURE DEPTH 3 IN Sales"

# Full project including system modules
mxcli -p app.mpr -c "SHOW STRUCTURE ALL"

When to use SHOW STRUCTURE vs SHOW + DESCRIBE

GoalCommand
“What modules are in this project?”SHOW STRUCTURE DEPTH 1
“What does module X contain?”SHOW STRUCTURE IN X
“List all entities (just names)”SHOW ENTITIES
“What are the attributes of entity X?”DESCRIBE ENTITY X
“Give me a complete overview of everything”SHOW STRUCTURE DEPTH 3

SHOW STRUCTURE is best for orientation – understanding the shape of the project at a glance. For detailed work on specific elements, switch to DESCRIBE.

What is next

Now that you can explore a project, you are ready to start making changes. Continue to Your First Changes to learn how to create entities, microflows, and pages using MDL.

Your First Changes

Now that you can explore a project with SHOW and DESCRIBE, it’s time to make changes. This chapter walks through the three most common modifications: creating an entity, a microflow, and a page.

Before you start

Always work on a copy. mxcli writes directly to your .mpr file. Before making changes, either:

  • Copy the .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

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

Over the next few pages, you’ll create:

WhatMDL statementPage
A Product entity with attributesCREATE PERSISTENT ENTITYCreating an Entity
A microflow that creates productsCREATE MICROFLOWCreating a Microflow
An overview page listing productsCREATE PAGECreating a Page

The final page, Validating with mxcli check, covers the full validation workflow in detail.

Quick note on module names

Every MDL statement references a module. In these examples we use MyModule – replace it with whatever module exists in your project. You can check available modules with:

mxcli -p app.mpr -c "SHOW MODULES"

Idempotent scripts with OR MODIFY

If you plan to run a script more than once (common during development), use CREATE OR MODIFY instead of plain CREATE. This updates the entity if it already exists instead of failing:

CREATE OR MODIFY PERSISTENT ENTITY MyModule.Product (
    Name: String(200) NOT NULL,
    Price: Decimal,
    IsActive: Boolean DEFAULT true
);

The OR MODIFY variant is available for entities, enumerations, microflows, and pages. You’ll see it used throughout the tutorial.

Creating an Entity

Entities are the foundation of any Mendix application – they define your data model. In this page you’ll create a Product entity with several attributes, verify it, and then link it to another entity with an association.

Create a simple entity

The CREATE PERSISTENT ENTITY statement defines an entity that is stored in the database. Attributes go inside parentheses, separated by commas:

CREATE PERSISTENT ENTITY MyModule.Product (
    Name: String(200) NOT NULL,
    Price: Decimal,
    IsActive: Boolean DEFAULT true
);

Let’s break this down:

PartMeaning
PERSISTENTThe entity is stored in the database (as opposed to NON-PERSISTENT, which exists only in memory)
MyModule.ProductFully qualified name: module dot entity name
String(200)A string attribute with a maximum length of 200 characters
NOT NULLThe attribute is required – it cannot be left empty
DecimalA decimal number (no precision/scale needed for basic use)
Boolean DEFAULT trueA boolean that defaults to true when a new object is created

Run it from the command line:

mxcli -p app.mpr -c "CREATE PERSISTENT ENTITY MyModule.Product (Name: String(200) NOT NULL, Price: Decimal, IsActive: Boolean DEFAULT true);"

Or save it to a file and execute:

mxcli exec create-product.mdl -p app.mpr

Verify the entity

Use DESCRIBE ENTITY to confirm your entity was created correctly:

mxcli -p app.mpr -c "DESCRIBE ENTITY MyModule.Product"

This prints the full MDL definition of the entity, including all attributes and their types. You should see Name, Price, and IsActive listed.

Add an association

Associations link entities together. To connect Product to an existing Order entity, create a reference association:

CREATE ASSOCIATION MyModule.Order_Product
    FROM MyModule.Order TO MyModule.Product
    TYPE Reference;

This creates a many-to-one relationship: each Order can reference one Product. Use ReferenceSet instead of Reference for many-to-many.

The naming convention Order_Product follows Mendix best practices – the “from” entity name comes first, then the “to” entity.

Association types

TypeMeaningExample
ReferenceMany-to-one (or one-to-one)An Order references one Product
ReferenceSetMany-to-manyAn Order can have multiple Products

Delete behavior

You can specify what happens when the “to” entity is deleted:

CREATE ASSOCIATION MyModule.Order_Product
    FROM MyModule.Order TO MyModule.Product
    TYPE Reference
    DELETE_BEHAVIOR PREVENT;

Options: PREVENT (block deletion if referenced), DELETE (cascade delete), or leave it out for the default behavior.

Using OR MODIFY for idempotent scripts

If you want to run the same script repeatedly without errors, use CREATE OR MODIFY:

CREATE OR MODIFY PERSISTENT ENTITY MyModule.Product (
    Name: String(200) NOT NULL,
    Price: Decimal,
    IsActive: Boolean DEFAULT true,
    Description: String(unlimited)
);

If the entity already exists, this updates it to match the new definition. If it doesn’t exist, it creates it. This is especially useful during iterative development.

More attribute types

Here’s a fuller example showing the attribute types you’ll use most often:

CREATE PERSISTENT ENTITY MyModule.Product (
    Name: String(200) NOT NULL,
    Description: String(unlimited),
    Price: Decimal,
    Quantity: Integer,
    Weight: Long,
    IsActive: Boolean DEFAULT true,
    CreatedDate: DateTime,
    Status: MyModule.ProductStatus
);

The last attribute references an enumeration (MyModule.ProductStatus). You’d create that separately:

CREATE ENUMERATION MyModule.ProductStatus (
    Active 'Active',
    Discontinued 'Discontinued',
    OutOfStock 'Out of Stock'
);

Extending a system entity

To create an entity that inherits from a system entity (like System.Image for file storage), use EXTENDS. Note that EXTENDS must come before the opening parenthesis:

-- Correct: EXTENDS before (
CREATE PERSISTENT ENTITY MyModule.ProductPhoto EXTENDS System.Image (
    PhotoCaption: String(200)
);
-- Wrong: EXTENDS after ( -- this will cause a parse error
CREATE PERSISTENT ENTITY MyModule.ProductPhoto (
    PhotoCaption: String(200)
) EXTENDS System.Image;

Common mistakes

String needs an explicit length. String alone is not valid – you must specify the maximum length:

-- Wrong
Name: String

-- Correct
Name: String(200)

-- For unlimited length
Description: String(unlimited)

EXTENDS must come before the parenthesis. See the example above. This is a common source of parse errors.

Module must exist. The module in the qualified name (e.g., MyModule in MyModule.Product) must already exist in the project. Check with SHOW MODULES.

Creating a Microflow

Microflows are the server-side logic of a Mendix application. They’re comparable to functions or methods in traditional programming. In this page you’ll create a microflow that accepts parameters, creates an object, commits it to the database, and returns it.

Create a simple microflow

This microflow takes a name and price, creates a Product object, commits it, and returns it:

CREATE MICROFLOW MyModule.CreateProduct(
    DECLARE $Name: String,
    DECLARE $Price: Decimal
)
RETURN MyModule.Product
BEGIN
    CREATE $Product: MyModule.Product (
        Name = $Name,
        Price = $Price,
        IsActive = true
    );
    COMMIT $Product;
    RETURN $Product;
END;

Let’s walk through each part:

PartMeaning
MyModule.CreateProductFully qualified microflow name
DECLARE $Name: StringInput parameter – a string value passed by the caller
RETURN MyModule.ProductThe microflow returns a Product entity object
BEGIN ... ENDThe microflow body
CREATE $Product: MyModule.Product (...)Creates a new Product object in memory and sets its attributes
COMMIT $ProductPersists the object to the database
RETURN $ProductReturns the committed object to the caller

Save this to a file (e.g., create-product-mf.mdl) and execute:

mxcli check create-product-mf.mdl
mxcli exec create-product-mf.mdl -p app.mpr

Verify the microflow

Use DESCRIBE MICROFLOW to see the generated MDL:

mxcli -p app.mpr -c "DESCRIBE MICROFLOW MyModule.CreateProduct"

This shows the full microflow definition, including parameters, activities, and the return type.

Understanding variables

All variables in MDL start with $. There are two contexts where you declare them:

Parameters (in the signature):

CREATE MICROFLOW MyModule.DoSomething(
    DECLARE $Customer: MyModule.Customer,
    DECLARE $Count: Integer
)

Local variables (inside BEGIN…END):

BEGIN
    DECLARE $Total: Integer = 0;
    DECLARE $Message: String = 'Processing...';
    DECLARE $Items: List of MyModule.Item = empty;
END;

For entity parameters, do not assign a default value – just declare the type:

-- Correct: entity parameter with no default
DECLARE $Customer: MyModule.Customer

-- Wrong: entity parameter with = empty
DECLARE $Customer: MyModule.Customer = empty

Retrieving objects

Use RETRIEVE to fetch objects from the database:

CREATE MICROFLOW MyModule.GetActiveProducts()
RETURN List of MyModule.Product
BEGIN
    RETRIEVE $Products: List of MyModule.Product
        FROM MyModule.Product
        WHERE IsActive = true;
    RETURN $Products;
END;

To retrieve a single object, add LIMIT 1:

RETRIEVE $Product: MyModule.Product
    FROM MyModule.Product
    WHERE Name = $SearchName
    LIMIT 1;

Conditional logic

Use IF ... THEN ... ELSE ... END IF for branching:

CREATE MICROFLOW MyModule.UpdateProductStatus(
    DECLARE $Product: MyModule.Product
)
RETURN Boolean
BEGIN
    IF $Product/Price > 0 THEN
        CHANGE $Product (IsActive = true);
        COMMIT $Product;
        RETURN true;
    ELSE
        LOG WARNING 'Product has no price set';
        RETURN false;
    END IF;
END;

Looping over a list

Use LOOP ... IN ... BEGIN ... END LOOP to iterate:

CREATE MICROFLOW MyModule.DeactivateProducts(
    DECLARE $Products: List of MyModule.Product
)
RETURN Boolean
BEGIN
    LOOP $Product IN $Products
    BEGIN
        CHANGE $Product (IsActive = false);
        COMMIT $Product;
    END LOOP;
    RETURN true;
END;

Note: the list $Products is a parameter. Never create an empty list variable and then loop over it – accept the list as a parameter instead.

Error handling

Add ON ERROR to handle failures on individual activities:

COMMIT $Product ON ERROR CONTINUE;

Options are CONTINUE (ignore the error and proceed), ROLLBACK (roll back the transaction and continue), or an inline error handler block:

COMMIT $Product ON ERROR {
    LOG ERROR 'Failed to commit product';
    RETURN false;
};

Organizing with folders

Place microflows in folders using the FOLDER keyword:

CREATE MICROFLOW MyModule.CreateProduct(
    DECLARE $Name: String,
    DECLARE $Price: Decimal
)
RETURN MyModule.Product
FOLDER 'ACT'
BEGIN
    CREATE $Product: MyModule.Product (
        Name = $Name,
        Price = $Price,
        IsActive = true
    );
    COMMIT $Product;
    RETURN $Product;
END;

This places the microflow in the ACT folder within the module, following the common Mendix convention of grouping microflows by type (ACT for actions, VAL for validations, SUB for sub-microflows).

Common mistakes

Every flow path must end with RETURN. If your microflow has IF/ELSE branches, each branch needs its own RETURN statement (or the return can come after END IF if both branches converge).

COMMIT is required to persist changes. CREATE and CHANGE only modify the object in memory. Without COMMIT, changes are lost when the microflow ends.

Entity parameters don’t use = empty. Declare them with just the type. Assigning = empty is for list variables inside the microflow body, not for parameters.

Creating a Page

Pages are the user interface of a Mendix application. In this page you’ll create an overview page that lists products in a data grid, and then an edit page with a form for modifying a single product.

Prerequisites

Before creating a page, you need two things:

  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

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:

PartMeaning
MyModule.ProductOverviewFully qualified page name
LAYOUT Atlas_Core.Atlas_DefaultThe 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.ProductA data grid that loads Product objects from the database
COLUMN NameA column showing the Name attribute

Execute it:

mxcli -p app.mpr -c "CREATE PAGE MyModule.ProductOverview LAYOUT Atlas_Core.Atlas_Default TITLE 'Products' (DATAGRID SOURCE DATABASE MyModule.Product (COLUMN Name, COLUMN Price, COLUMN IsActive));"

Or save to a file and run:

mxcli check create-pages.mdl
mxcli exec create-pages.mdl -p app.mpr

Verify the page

mxcli -p app.mpr -c "DESCRIBE PAGE MyModule.ProductOverview"

This prints the full page definition with all widgets and their properties.

Create an edit page

Edit pages use a DataView to display and edit a single object. The object is passed as a page parameter:

CREATE PAGE MyModule.Product_Edit
(
    Params: { $Product: MyModule.Product },
    Title: 'Edit Product',
    Layout: Atlas_Core.PopupLayout
)
{
    DATAVIEW dvProduct (DataSource: $Product) {
        TEXTBOX txtName (Label: 'Name', Attribute: Name)
        TEXTBOX txtPrice (Label: 'Price', Attribute: Price)
        CHECKBOX cbActive (Label: 'Active', Attribute: IsActive)

        FOOTER footer1 {
            ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)
            ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES)
        }
    }
};

Key differences from the overview page:

PartMeaning
Params: { $Product: MyModule.Product }The page expects a Product object to be passed when opened
Layout: Atlas_Core.PopupLayoutUses a popup/dialog layout instead of a full page
DATAVIEW dvProduct (DataSource: $Product)Binds to the page parameter
TEXTBOX, CHECKBOXInput widgets bound to entity attributes
FOOTERA section at the bottom of the DataView for action buttons
Action: SAVE_CHANGESBuilt-in action that commits the object and closes the page
Action: CANCEL_CHANGESBuilt-in action that rolls back changes and closes the page

Notice the two different page syntaxes: the overview page uses the compact syntax (LAYOUT and TITLE as keywords before parentheses), while the edit page uses the property syntax (properties inside a (Key: value) block followed by a { widget tree } block). Both are valid – use whichever fits better.

Widget reference

Here are the most commonly used widgets:

Input widgets

TEXTBOX txtName (Label: 'Name', Attribute: Name)
TEXTAREA txtDesc (Label: 'Description', Attribute: Description)
CHECKBOX cbActive (Label: 'Active', Attribute: IsActive)
DATEPICKER dpCreated (Label: 'Created', Attribute: CreatedDate)
COMBOBOX cbStatus (Label: 'Status', Attribute: Status)
RADIOBUTTONS rbType (Label: 'Type', Attribute: ProductType)

Display widgets

DYNAMICTEXT dynName (Attribute: Name)

Action buttons

ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)
ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES)
ACTIONBUTTON btnDelete (Caption: 'Delete', Action: DELETE, ButtonStyle: Danger)
ACTIONBUTTON btnProcess (Caption: 'Process', Action: MICROFLOW MyModule.ACT_ProcessProduct(Product: $Product))

Layout widgets

CONTAINER cntWrapper (Class: 'card') {
    -- child widgets go here
}

LAYOUTGRID lgMain {
    ROW r1 {
        COLUMN c1 (Weight: 6) { ... }
        COLUMN c2 (Weight: 6) { ... }
    }
}

Embedding a snippet

To reuse a page fragment, call a snippet:

SNIPPETCALL scProductCard (Snippet: MyModule.ProductCard)

The snippet must already exist in the project.

Common mistakes

Layout must exist in the project. If you specify a layout that doesn’t exist, the page will fail validation. Check available layouts with SHOW PAGES and look for documents of type Layout.

Widget names must be unique within a page. Every widget needs a name (e.g., txtName, btnSave), and these must not collide within the same page.

DataView needs a data source. A DataView must know where its data comes from – either a page parameter (DataSource: $Product), a microflow, or a nested context.

Validating with mxcli check

You’ve created entities, microflows, and pages. Before opening your project in Studio Pro, you should validate that everything is correct. mxcli provides three levels of validation, each catching different kinds of problems.

Level 1: Syntax check (no project needed)

The fastest check. It parses your MDL script and reports syntax errors without touching any .mpr file:

mxcli check script.mdl

This catches:

  • Typos in keywords (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

You can also check a single statement:

mxcli check -c "CREATE PERSISTENT ENTITY MyModule.Product (Name: String(200));"

Level 2: Syntax + reference validation

Add -p and --references to also verify that names in your script resolve to real elements in the project:

mxcli check script.mdl -p app.mpr --references

This catches everything Level 1 catches, plus:

  • References to entities that don’t exist (MyModule.NonExistent)
  • References to attributes that don’t exist on an entity
  • Microflow calls to non-existent microflows
  • Page layouts that don’t exist in the project
  • Association endpoints pointing to missing entities

This is the check you should run before executing a script. It’s fast (reads the project but doesn’t modify it) and catches most mistakes.

Level 3: Full project validation with mx check

After executing your script, validate the entire project using the Mendix toolchain:

mxcli docker check -p app.mpr

This runs Mendix’s own mx check tool inside a Docker container. It performs the same validation that Studio Pro does when you open a project, catching issues that mxcli’s parser cannot detect:

  • BSON serialization errors (malformed internal data)
  • Security configuration problems (missing access rules, CE0066 errors)
  • Widget definition mismatches (CE0463 errors)
  • Missing required properties on widgets
  • Broken cross-references between documents

The first time you run this, it will download the MxBuild toolchain for your project’s Mendix version. Subsequent runs reuse the cached download.

Without Docker

If you have mx installed locally (e.g., from a Mendix installation), you can run the check directly:

~/.mxcli/mxbuild/*/modeler/mx check app.mpr

Or use mxcli to auto-download and run it:

mxcli setup mxbuild -p app.mpr
~/.mxcli/mxbuild/*/modeler/mx check app.mpr

Here’s the workflow you should follow for every change:

Write MDL --> Check syntax --> Execute --> Docker check --> Open in Studio Pro

In practice:

# 1. Write your MDL in a script file
cat > changes.mdl << 'EOF'
CREATE OR MODIFY PERSISTENT ENTITY MyModule.Product (
    Name: String(200) NOT NULL,
    Price: Decimal,
    IsActive: Boolean DEFAULT true
);

CREATE OR MODIFY MICROFLOW MyModule.CreateProduct(
    DECLARE $Name: String,
    DECLARE $Price: Decimal
)
RETURN MyModule.Product
BEGIN
    CREATE $Product: MyModule.Product (
        Name = $Name,
        Price = $Price,
        IsActive = true
    );
    COMMIT $Product;
    RETURN $Product;
END;
EOF

# 2. Check syntax (fast, no project needed)
mxcli check changes.mdl

# 3. Check references (needs project, still fast)
mxcli check changes.mdl -p app.mpr --references

# 4. Execute against the project
mxcli exec changes.mdl -p app.mpr

# 5. Full validation
mxcli docker check -p app.mpr

# 6. Open in Studio Pro and verify visually

Previewing changes with diff

Before executing, you can preview what a script would change:

mxcli diff -p app.mpr changes.mdl

This compares the script against the current project state and shows what would be created, modified, or left unchanged. It does not modify the project.

What mxcli check catches automatically

Beyond basic syntax, mxcli check includes built-in anti-pattern detection:

PatternProblemWhat check reports
DECLARE $Items List of ... = empty followed by LOOP $Item IN $ItemsLooping over an empty list does nothingWarning: empty list variable used as loop source
Nested LOOP inside LOOP for list matchingO(N^2) performanceWarning: use RETRIEVE from list instead
Missing RETURN at end of flow pathMicroflow won’t compileError: missing return statement

Linting for deeper analysis

For a broader set of checks across the entire project (not just a single script), use the linter:

mxcli lint -p app.mpr

This runs 14 built-in rules plus 27 Starlark rules covering security, architecture, quality, and naming conventions. See mxcli lint --list-rules for the full list.

For CI/CD integration, output in SARIF format:

mxcli lint -p app.mpr --format sarif > results.sarif

Working with AI Assistants

mxcli is built from the ground up to work with AI coding assistants. The mxcli init command sets up your Mendix project with skills, configuration files, and a dev container so that AI tools can read and modify your project using MDL.

Why AI + MDL?

Mendix projects are stored in binary .mpr files. AI assistants cannot read or edit binary files directly. mxcli solves this by providing MDL – a text-based, SQL-like language that describes Mendix model elements. An AI assistant uses mxcli commands to explore the project, writes MDL scripts to make changes, validates them, and executes them against the .mpr file.

The result: you describe what you want in natural language, and the AI builds it in your Mendix project.

The token efficiency advantage

When working with AI APIs, context window size is a critical constraint. MDL’s compact syntax provides a significant advantage over JSON model representations:

RepresentationTokens for a 10-entity module
JSON (raw model)~15,000–25,000 tokens
MDL~2,000–4,000 tokens
Savings5–10x fewer tokens

Fewer tokens means lower API costs, more of your application fits in a single prompt, and the AI produces better results because there is less noise in the context.

Supported AI tools

mxcli supports five AI coding assistants out of the box, plus a universal format that works with any tool:

ToolInit FlagConfig FileDescription
Claude Code--tool claude (default).claude/, CLAUDE.mdFull integration with skills, commands, and lint rules
Cursor--tool cursor.cursorrulesCompact MDL reference and command guide
Continue.dev--tool continue.continue/config.jsonCustom commands and slash commands
Windsurf--tool windsurf.windsurfrulesCodeium’s AI with MDL rules
Aider--tool aider.aider.conf.ymlTerminal-based AI pair programming
Universal(always created)AGENTS.md, .ai-context/Works with all tools
# List all supported tools
mxcli init --list-tools

How it works at a glance

The workflow is the same regardless of which AI tool you use:

  1. Initializemxcli 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, Cursor, Continue.dev, 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 validatesmxcli 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 is the primary AI integration for mxcli. It gets the deepest support: a dedicated configuration directory, project-level context in CLAUDE.md, skill files that teach Claude MDL patterns, and slash commands for common operations.

Initializing a project

Claude Code is the default tool, so you don’t need to specify a flag:

mxcli init /path/to/my-mendix-project

This is equivalent to:

mxcli init --tool claude /path/to/my-mendix-project

What gets created

After running mxcli init, your project directory gains several new entries:

my-mendix-project/
├── CLAUDE.md                    # Project context for Claude Code
├── AGENTS.md                    # Universal AI assistant guide
├── .claude/
│   ├── settings.json            # Claude Code settings
│   ├── commands/                # Slash commands (/create-entity, etc.)
│   └── lint-rules/              # Starlark lint rules
├── .ai-context/
│   ├── skills/                  # MDL pattern guides (shared by all tools)
│   └── examples/                # Example MDL scripts
├── .devcontainer/
│   ├── devcontainer.json        # Dev container configuration
│   └── Dockerfile               # Container image with mxcli, JDK, Docker
├── mxcli                        # CLI binary (copied into project)
└── app.mpr                      # Your Mendix project (already existed)

CLAUDE.md

The CLAUDE.md file gives Claude Code project-level context. It describes what mxcli is, lists the available MDL commands, and tells Claude to read the skill files before writing MDL. This file is automatically read by Claude Code when it starts.

Skills

The .claude/skills/ directory (and .ai-context/skills/ for the universal copy) contains markdown files that teach Claude specific MDL patterns. For example, write-microflows.md explains microflow syntax, common mistakes, and a validation checklist. Claude reads the relevant skill before generating MDL, which dramatically improves output quality.

Commands

The .claude/commands/ directory contains slash commands that you can invoke from within Claude Code. These provide shortcuts for common operations.

Setting up the dev container

The dev container is the recommended way to work with Claude Code. It sandboxes the AI so it can only access your project files, and it comes pre-configured with everything you need.

What’s installed in the container

ComponentPurpose
mxcliMendix CLI (copied into project root)
MxBuild / mxMendix project validation and building
JDK 21 (Adoptium)Required by MxBuild
Docker-in-DockerRunning Mendix apps locally with mxcli docker
Node.jsPlaywright testing support
PostgreSQL clientDatabase connectivity
Claude CodeAuto-installed when the container starts

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

With the dev container running, open a terminal in VS Code and start Claude:

claude

Claude Code now has access to your project files, the mxcli binary, and all the skill files. You can start asking it to do things.

How Claude works with your project

Claude follows a consistent pattern when you ask it to modify your Mendix project:

1. Explore

Claude uses mxcli commands to understand your project before making changes:

-- What modules exist?
SHOW MODULES;

-- What entities are in this module?
SHOW ENTITIES IN Sales;

-- What does this entity look like?
DESCRIBE ENTITY Sales.Customer;

-- What microflows exist?
SHOW MICROFLOWS IN Sales;

-- Search for something specific
SEARCH 'validation';

2. Read the relevant skill

Before writing MDL, Claude reads the appropriate skill file. If you ask for a microflow, it reads write-microflows.md. If you ask for a page, it reads create-page.md. The skills contain syntax references, examples, and validation checklists.

3. Write MDL

Claude generates an MDL script based on what it learned from the project and the skill files:

/** Customer master data */
@Position(100, 100)
CREATE PERSISTENT ENTITY Sales.Customer (
    Name: String(200) NOT NULL,
    Email: String(200) NOT NULL,
    Phone: String(50),
    IsActive: Boolean DEFAULT true
);

4. Validate

Claude checks the script for syntax errors and reference issues:

./mxcli check script.mdl
./mxcli check script.mdl -p app.mpr --references

5. Execute

If validation passes, Claude runs the script against your project:

./mxcli -p app.mpr -c "EXECUTE SCRIPT 'script.mdl'"

6. Verify

Claude can run a full project check using the Mendix build tools:

./mxcli docker check -p app.mpr

Example interaction

Here is a typical conversation with Claude Code:

You: Create a Customer entity in the Sales module with name, email, and phone. Then create an overview page that shows all customers in a data grid.

Claude (explores the project):

./mxcli -p app.mpr -c "SHOW MODULES"
./mxcli -p app.mpr -c "SHOW ENTITIES IN Sales"
./mxcli -p app.mpr -c "SHOW PAGES IN Sales"

Claude (reads skills, writes MDL, validates, and executes):

/** Customer contact information */
@Position(100, 100)
CREATE PERSISTENT ENTITY Sales.Customer (
    Name: String(200) NOT NULL,
    Email: String(200) NOT NULL,
    Phone: String(50),
    IsActive: Boolean DEFAULT true,
    CreatedAt: DateTime DEFAULT '[%CurrentDateTime%]'
);

/** Customer overview page with data grid */
CREATE PAGE Sales.Customer_Overview
(
  Title: 'Customers',
  Layout: Atlas_Core.Atlas_Default
)
{
  LAYOUTGRID mainGrid {
    ROW row1 {
      COLUMN col1 (DesktopWidth: 12) {
        DATAGRID dgCustomers (
          DataSource: DATABASE FROM Sales.Customer SORT BY Name ASC
        ) {
          COLUMN colName (Attribute: Name, Caption: 'Name')
          COLUMN colEmail (Attribute: Email, Caption: 'Email')
          COLUMN colPhone (Attribute: Phone, Caption: 'Phone')
          COLUMN colActive (Attribute: IsActive, Caption: 'Active')
        }
      }
    }
  }
}

Claude validates the script, executes it, and reports back. You can then open the project in Studio Pro to review the result.

Tips for working with Claude Code

  • Be specific about module names. Say “Create a Customer entity in the Sales module” rather than just “Create a Customer entity.”
  • Mention existing elements. If you want an association to an existing entity, name it: “Link Order to the existing Sales.Customer entity.”
  • Let Claude explore first. If you’re asking for changes to an existing project, Claude will run SHOW and DESCRIBE commands to understand what’s already there. This leads to better results than trying to describe the current state yourself.
  • Review in Studio Pro. After Claude makes changes, open the project in Studio Pro to verify everything looks right visually.
  • Use mxcli docker check to catch issues that mxcli check alone might miss. The Mendix build tools perform deeper validation.

Next steps

If you use other AI tools alongside Claude Code, see Cursor / Continue.dev / Windsurf. To understand how skills work in detail, see Skills and CLAUDE.md.

Cursor / Continue.dev / Windsurf

Claude Code is the default integration, but mxcli also supports Cursor, Continue.dev, Windsurf, and Aider. Each tool gets its own configuration file that teaches the AI about MDL syntax and mxcli commands.

Initializing for a specific tool

Use the --tool flag to specify which AI tool you use:

# Cursor
mxcli init --tool cursor /path/to/my-mendix-project

# Continue.dev
mxcli init --tool continue /path/to/my-mendix-project

# Windsurf
mxcli init --tool windsurf /path/to/my-mendix-project

# Aider
mxcli init --tool aider /path/to/my-mendix-project

Setting up multiple tools

You can configure several tools at once. This is useful if different team members use different editors, or if you want to try several tools on the same project:

# Multiple tools
mxcli init --tool claude --tool cursor /path/to/my-mendix-project

# All supported tools at once
mxcli init --all-tools /path/to/my-mendix-project

Adding a tool to an existing project

If you already ran mxcli init and want to add support for another tool without re-initializing:

mxcli add-tool cursor
mxcli add-tool windsurf

This creates the tool-specific config file without touching the existing setup.

What each tool gets

Every tool gets the universal files that are always created:

FilePurpose
AGENTS.mdComprehensive 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
mxcliCLI binary

On top of the universal files, each tool gets its own configuration:

ToolConfig FileContents
Claude Code.claude/, CLAUDE.mdSettings, skills, commands, lint rules, project context
Cursor.cursorrulesCompact MDL reference and mxcli command guide
Continue.dev.continue/config.jsonCustom commands and slash commands
Windsurf.windsurfrulesMDL rules for Codeium’s AI
Aider.aider.conf.ymlYAML configuration for Aider

Tool details

Cursor

Cursor reads its instructions from .cursorrules in the project root. The file mxcli generates contains a compact MDL syntax reference and a list of mxcli commands the AI can use. Cursor’s Composer and Chat features will reference this file automatically.

mxcli init --tool cursor /path/to/project

Created files:

  • .cursorrules – MDL syntax, command reference, and 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

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 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 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.

The universal format: AGENTS.md

Regardless of which tool you pick, mxcli always creates AGENTS.md and the .ai-context/ directory. These use a universal format that most AI tools understand:

  • AGENTS.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

To see all tools mxcli knows about:

mxcli init --list-tools

Next steps

To understand what the skill files contain and how they guide AI behavior, see Skills and CLAUDE.md.

Skills and CLAUDE.md

Skills are markdown files that teach AI assistants how to write correct MDL. Each skill covers a specific topic – creating pages, writing microflows, managing security – and contains syntax references, examples, and validation checklists. When an AI assistant needs to generate MDL, it reads the relevant skill first, which dramatically improves output quality.

Where skills live

Skills are installed in two locations:

LocationUsed by
.claude/skills/Claude Code (tool-specific copy)
.ai-context/skills/All tools (universal copy)

Both directories contain the same files. The .claude/skills/ copy exists because Claude Code has a built-in mechanism for reading files from its .claude/ directory. The .ai-context/skills/ copy is the universal location that any AI tool can access.

Available skills

mxcli init installs the following skill files:

Skill FileTopic
generate-domain-model.mdEntity, attribute, and association syntax
write-microflows.mdMicroflow syntax, activities, common mistakes
create-page.mdPage and widget syntax reference
alter-page.mdALTER PAGE/SNIPPET for modifying existing pages
overview-pages.mdCRUD page patterns (overview + edit)
master-detail-pages.mdMaster-detail page patterns
manage-security.mdModule roles, user roles, access control, GRANT/REVOKE
manage-navigation.mdNavigation profiles, home pages, menus
demo-data.mdMendix ID system, association storage, demo data insertion
xpath-constraints.mdXPath syntax in WHERE clauses, nested predicates
database-connections.mdExternal database connections from microflows
check-syntax.mdPre-flight validation checklist
organize-project.mdFolders, MOVE command, project structure conventions
test-microflows.mdTest annotations, file formats, Docker setup
patterns-data-processing.mdDelta merge, batch processing, list operations

What a skill file contains

A typical skill file has four sections:

1. Syntax reference

The core MDL syntax for the topic, with all available options and keywords:

-- From write-microflows.md:
CREATE MICROFLOW Module.Name(
    $param: EntityType
) RETURNS ReturnType AS $result
BEGIN
    -- activities here
END;

2. Examples

Complete, working MDL examples that the AI can use as templates:

-- From create-page.md:
CREATE PAGE Sales.Customer_Overview
(
  Title: 'Customers',
  Layout: Atlas_Core.Atlas_Default
)
{
  DATAGRID dgCustomers (
    DataSource: DATABASE FROM Sales.Customer SORT BY Name ASC
  ) {
    COLUMN colName (Attribute: Name, Caption: 'Name')
  }
}

3. Common mistakes

A list of errors the AI should avoid. For example, write-microflows.md warns against creating empty list variables as loop sources, and create-page.md documents required widget properties that are easy to forget.

4. Validation checklist

Steps the AI should follow after writing MDL to confirm correctness:

# Syntax check (no project needed)
./mxcli check script.mdl

# Syntax + reference validation
./mxcli check script.mdl -p app.mpr --references

CLAUDE.md

The CLAUDE.md file is specific to Claude Code. It sits in the project root and is automatically read by Claude when it starts. It provides:

  • Project overview – what the project is, which modules exist, and what mxcli is
  • Available commands – a summary of mxcli commands Claude can use
  • Rules – instructions like “always read the relevant skill file before writing MDL” and “always validate with mxcli 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

AGENTS.md serves the same purpose as CLAUDE.md but in a universal format. It is always created by mxcli init, regardless of which tool you selected. AI tools that don’t have their own config format (or that read markdown files from the project root) will pick up AGENTS.md automatically.

Adding custom skills

You can create your own skill files to teach the AI about your project’s patterns and conventions. Add markdown files to .ai-context/skills/ (or .claude/skills/ for Claude Code):

.ai-context/skills/
├── write-microflows.md           # Built-in (installed by mxcli init)
├── create-page.md                # Built-in
├── our-naming-conventions.md     # Custom: your team's naming rules
├── order-processing-pattern.md   # Custom: how orders work in your app
└── api-integration-guide.md      # Custom: how to call external APIs

A custom skill file is just a markdown document. Write it the same way you would explain something to a new team member:

# Order Processing Pattern

When creating microflows that process orders in our application, follow these rules:

1. Always validate the order has at least one OrderLine
2. Use the Sales.OrderStatus enumeration for status tracking
3. Log to the 'OrderProcessing' node at INFO level
4. Send a confirmation email via Sales.SendNotification

## Example

\```sql
CREATE MICROFLOW Sales.ACT_Order_Submit($order: Sales.Order)
RETURNS Boolean AS $success
BEGIN
    -- validation, processing, notification...
END;
\```

The AI will read your custom skills alongside the built-in ones, learning your project’s specific patterns.

How the AI uses skills

The workflow is straightforward:

  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

Now that you understand how skills guide AI behavior, see The MDL + AI Workflow for the complete recommended workflow from project initialization to review in Studio Pro.

The MDL + AI Workflow

This page walks through the complete recommended workflow for using mxcli with an AI assistant, from project setup to reviewing the result in Studio Pro.

The ten-step workflow

Step 1: Initialize the project

Start by running mxcli init on your Mendix project:

mxcli init /path/to/my-mendix-project

This creates the configuration files, skill documents, and dev container setup. If you use a tool other than Claude Code, specify it with --tool:

mxcli init --tool cursor /path/to/my-mendix-project

Step 2: Open in a dev container

Open the project folder in VS Code (or Cursor). VS Code will detect the .devcontainer/ directory and prompt you to reopen in a container. Click “Reopen in Container”.

The dev container provides a sandboxed environment where the AI assistant can only access your project files. It comes with mxcli, JDK, Docker, and your AI tool pre-installed.

Step 3: Start your AI assistant

Once inside the dev container, open a terminal and start your AI tool:

# Claude Code
claude

# Or use Cursor's Composer, Continue.dev's sidebar, etc.

Step 4: Describe what you want

Tell the AI what you need in plain language. Be specific about module names and mention existing elements:

“Create a Product entity in the Sales module with name, price, and description. Add an association to the existing Sales.Category entity.”

Better prompts lead to better results. A few tips:

  • Name the module explicitly: “in the Sales module”
  • Reference existing elements: “linked to the existing Customer entity”
  • Describe the behavior, not the implementation: “a microflow that validates the email format” rather than “an IF statement that checks for @ and .”

Step 5: The AI explores your project

Before making changes, the AI uses mxcli to understand the current state of your project:

-- See what modules exist
SHOW MODULES;

-- Check what's already in the Sales module
SHOW STRUCTURE IN Sales;

-- Look at an existing entity for context
DESCRIBE ENTITY Sales.Category;

-- Search for related elements
SEARCH 'product';

This exploration step is important. The AI needs to know what entities, associations, and microflows already exist so it can write MDL that fits with your project.

Step 6: The AI writes MDL

Guided by the skill files and its exploration, the AI writes an MDL script:

/** Product catalog item */
@Position(300, 100)
CREATE PERSISTENT ENTITY Sales.Product (
    Name: String(200) NOT NULL,
    Description: String(0),
    Price: Decimal NOT NULL DEFAULT 0,
    IsActive: Boolean DEFAULT true
);

/** Link products to categories */
CREATE ASSOCIATION Sales.Product_Category
    FROM Sales.Product
    TO Sales.Category
    TYPE Reference
    OWNER Default;

Step 7: The AI validates

Before executing, the AI checks for errors:

# Check syntax
./mxcli check script.mdl

# Check syntax and verify that referenced elements exist
./mxcli check script.mdl -p app.mpr --references

If there are errors, the AI fixes them and re-validates. This cycle happens automatically – you don’t need to intervene.

Step 8: The AI executes

Once validation passes, the AI runs the script against your project:

./mxcli -p app.mpr -c "EXECUTE SCRIPT 'script.mdl'"

The changes are written directly to your .mpr file.

Step 9: Deep validation

For thorough validation, the AI can run the Mendix build tools:

./mxcli docker check -p app.mpr

This uses MxBuild (the same engine Studio Pro uses) to check the project for consistency errors, missing references, and other issues that mxcli check alone might miss.

Step 10: Review in Studio Pro

Open your project in Mendix Studio Pro to review the changes visually. Check that:

  • Entities appear correctly in the domain model
  • Pages render as expected
  • Microflow logic looks right in the visual editor
  • Security settings are correct

A complete example session

Here is what a full session looks like when you ask Claude Code to build a customer management feature:

You: “Create a customer management feature in the CRM module. I need a Customer entity with name, email, phone, and status. Create an overview page with a data grid and a popup edit form. Add a microflow that validates the email before saving.”

Claude Code:

  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

Be specific about what exists

If you’re working with an existing project, mention the elements that are already there:

“Add a ShippingAddress field to the existing CRM.Customer entity and update the CRM.Customer_Edit page to include it.”

Let the AI explore

Don’t try to describe your entire project upfront. The AI is good at exploring. A prompt like “look at the Sales module and add a discount field to orders” is enough – the AI will figure out the entity name, existing fields, and page structure on its own.

Iterate

You don’t have to get everything right in one prompt. Start with the entity, review it, then ask for pages, then microflows. Small, focused requests tend to produce better results than one massive prompt.

Use mxcli docker check for final validation

mxcli check validates MDL syntax and references, but it doesn’t catch everything. The Mendix build tools (mxcli docker check) perform the same validation that Studio Pro does. Use it as a final gate before considering the work done.

Version control

Always work on a copy of your project or use version control. mxcli writes directly to your .mpr file. If something goes wrong, you want to be able to revert. For MPR v2 projects (Mendix 10.18+), the individual document files in mprcontents/ work well with Git.

Add custom skills for your project

If your project has specific patterns or conventions, write them down as skill files. For example, if every entity needs an audit trail (CreatedBy, CreatedAt, ChangedBy, ChangedAt), create a skill that documents this convention. The AI will follow it consistently.

MDL Basics

MDL (Mendix Definition Language) is a SQL-like language for reading and modifying Mendix application projects. It provides a text-based alternative to the visual editors in Mendix Studio Pro.

What MDL Looks Like

MDL uses familiar SQL-style syntax with Mendix-specific extensions. Here is a simple example that creates an entity with attributes:

CREATE PERSISTENT ENTITY Sales.Customer (
  CustomerId: AutoNumber NOT NULL UNIQUE DEFAULT 1,
  Name: String(200) NOT NULL,
  Email: String(200) UNIQUE,
  IsActive: Boolean DEFAULT TRUE
)
INDEX (Name);

Statement Termination

Statements are terminated with a semicolon (;) or a forward slash (/) on its own line (Oracle-style, useful for multi-line statements):

-- Semicolon terminator
CREATE MODULE OrderManagement;

-- Forward-slash terminator (useful for long statements)
CREATE PERSISTENT ENTITY Sales.Order (
  OrderId: AutoNumber NOT NULL UNIQUE,
  OrderDate: DateTime NOT NULL
)
INDEX (OrderDate DESC);
/

Simple commands such as HELP, EXIT, STATUS, SHOW, and DESCRIBE do not require a terminator.

Case Insensitivity

All MDL keywords are case-insensitive. The following are equivalent:

CREATE PERSISTENT ENTITY Sales.Customer ( ... );
create persistent entity Sales.Customer ( ... );
Create Persistent Entity Sales.Customer ( ... );

Identifiers (module names, entity names, attribute names) are case-sensitive and must match the Mendix model exactly.

Statement Categories

MDL statements fall into several categories:

CategoryExamples
QuerySHOW ENTITIES, DESCRIBE ENTITY, SEARCH
Domain ModelCREATE ENTITY, CREATE ASSOCIATION, ALTER ENTITY
EnumerationsCREATE ENUMERATION, ALTER ENUMERATION
MicroflowsCREATE MICROFLOW, DROP MICROFLOW
PagesCREATE PAGE, ALTER PAGE, CREATE SNIPPET
SecurityGRANT, REVOKE, CREATE USER ROLE
NavigationCREATE OR REPLACE NAVIGATION
ConnectionCONNECT LOCAL, DISCONNECT, STATUS

Further Reading

Lexical Structure

This page describes the tokens that make up the MDL language: keywords, literals, and identifiers.

Keywords

MDL keywords are case-insensitive. The following are reserved keywords:

ACCESS, ACTIONS, ADD, AFTER, ALL, ALTER, AND, ANNOTATION, AS, ASC,
ASCENDING, ASSOCIATION, AUTONUMBER, BATCH, BEFORE, BEGIN, BINARY,
BOOLEAN, BOTH, BUSINESS, BY, CALL, CANCEL, CAPTION, CASCADE,
CATALOG, CHANGE, CHILD, CLOSE, COLUMN, COMBOBOX, COMMIT, CONNECT,
CONFIGURATION, CONNECTOR, CONSTANT, CONSTRAINT, CONTAINER, CREATE,
CRUD, DATAGRID, DATAVIEW, DATE, DATETIME, DECLARE, DEFAULT, DELETE,
DELETE_BEHAVIOR, DELETE_BUT_KEEP_REFERENCES, DELETE_CASCADE, DEMO,
DEPTH, DESC, DESCENDING, DESCRIBE, DIFF, DISCONNECT, DROP, ELSE,
EMPTY, END, ENTITY, ENUMERATION, ERROR, EVENT, EVENTS, EXECUTE,
EXEC, EXIT, EXPORT, EXTENDS, EXTERNAL, FALSE, FOLDER, FOOTER,
FOR, FORMAT, FROM, FULL, GALLERY, GENERATE, GRANT, HEADER, HELP,
HOME, IF, IMPORT, IN, INDEX, INFO, INSERT, INTEGER, INTO, JAVA,
KEEP_REFERENCES, LABEL, LANGUAGE, LAYOUT, LAYOUTGRID, LEVEL, LIMIT,
LINK, LIST, LISTVIEW, LOCAL, LOG, LOGIN, LONG, LOOP, MANAGE, MAP,
MATRIX, MENU, MESSAGE, MICROFLOW, MICROFLOWS, MODEL, MODIFY, MODULE,
MODULES, MOVE, NANOFLOW, NANOFLOWS, NAVIGATION, NODE, NON_PERSISTENT,
NOT, NULL, OF, ON, OR, ORACLE, OVERVIEW, OWNER, PAGE, PAGES, PARENT,
PASSWORD, PERSISTENT, POSITION, POSTGRES, PRODUCTION, PROJECT,
PROTOTYPE, QUERY, QUIT, REFERENCE, REFERENCESET, REFRESH, REMOVE,
REPLACE, REPORT, RESPONSIVE, RETRIEVE, RETURN, REVOKE, ROLE, ROLES,
ROLLBACK, ROW, SAVE, SCRIPT, SEARCH, SECURITY, SELECTION, SET, SHOW,
SNIPPET, SNIPPETS, SQL, SQLSERVER, STATUS, STRING, STRUCTURE,
TABLES, TEXTBOX, TEXTAREA, THEN, TO, TRUE, TYPE, UNIQUE, UPDATE,
USER, VALIDATION, VALUE, VIEW, VIEWS, VISIBLE, WARNING, WHERE, WIDGET,
WIDGETS, WITH, WORKFLOWS, WRITE

Most keywords work unquoted as identifiers (entity names, attribute names). Only structural keywords like CREATE, DELETE, BEGIN, END, RETURN, ENTITY, and MODULE require quoting when used as identifiers.

Literals

String Literals

String literals use single quotes:

'single quoted string'
'it''s here'            -- doubled single quote to escape

The only escape sequence is '' (two single quotes) to represent a literal single quote. Backslash escaping is not supported.

Numeric Literals

42          -- Integer
3.14        -- Decimal
-100        -- Negative integer
1.5e10      -- Scientific notation

Boolean Literals

TRUE
FALSE

Quoted Identifiers

When an identifier collides with a reserved keyword, use double quotes (ANSI SQL style) or backticks (MySQL style):

"ComboBox"."CategoryTreeVE"
`Order`.`Status`
"ComboBox".CategoryTreeVE    -- mixed quoting is allowed

See Qualified Names for more on identifier syntax and the Module.Name notation.

Qualified Names

MDL uses dot-separated qualified names to reference elements within a Mendix project. This naming convention ensures elements are unambiguous across modules.

Simple Identifiers

Valid identifier characters:

  • Letters: A-Z, a-z
  • Digits: 0-9 (not as first character)
  • Underscore: _
MyEntity
my_attribute
Attribute123

Qualified Name Format

The general format is Module.Element or Module.Entity.Attribute:

MyModule.Customer           -- Entity in module
MyModule.OrderStatus        -- Enumeration in module
MyModule.Customer.Name      -- Attribute in entity

Two-Part Names

Most elements use a two-part name: Module.ElementName.

Sales.Customer              -- entity
Sales.OrderStatus           -- enumeration
Sales.ACT_CreateOrder       -- microflow
Sales.Customer_Edit         -- page
Sales.Order_Customer        -- association

Three-Part Names

Attribute references use three parts: Module.Entity.Attribute.

Sales.Customer.Name         -- attribute on entity
Sales.Order.TotalAmount     -- attribute on entity

Quoting Rules

When a name segment is a reserved keyword, wrap it in double quotes or backticks:

"ComboBox"."CategoryTreeVE"
`Order`.`Status`

Mixed quoting is allowed – you only need to quote the segments that conflict:

"ComboBox".CategoryTreeVE
Sales."Order"

Identifiers that contain only letters, digits, and underscores (and do not start with a digit) never need quoting.

Usage in Statements

Qualified names appear throughout MDL:

-- Entity creation
CREATE PERSISTENT ENTITY Sales.Customer ( ... );

-- Association referencing two entities
CREATE ASSOCIATION Sales.Order_Customer
  FROM Sales.Customer
  TO Sales.Order
  TYPE Reference;

-- Enumeration reference in an attribute type
Status: Enumeration(Sales.OrderStatus) DEFAULT 'Active'

-- Microflow call
$Result = CALL MICROFLOW Sales.ACT_ProcessOrder ($Order = $Order);

See Also

Comments and Documentation

MDL supports three comment styles for annotating scripts and attaching documentation to model elements.

Single-Line Comments

Use -- (SQL style) or // (C style) for single-line comments:

-- This is a single-line comment
SHOW MODULES

// This is also a single-line comment
SHOW ENTITIES IN Sales

Everything after the comment marker to the end of the line is ignored.

Multi-Line Comments

Use /* ... */ for comments that span multiple lines:

/* This comment spans
   multiple lines and is useful
   for longer explanations */
CREATE PERSISTENT ENTITY Sales.Customer (
  Name: String(200) NOT NULL
);

Documentation Comments

Use /** ... */ to attach documentation to model elements. Documentation comments are stored in the Mendix model and appear in Studio Pro:

/** Customer entity stores master customer data.
 *  Used by the Sales and Support modules.
 */
@Position(100, 200)
CREATE PERSISTENT ENTITY Sales.Customer (
  /** Unique customer identifier, auto-generated */
  CustomerId: AutoNumber NOT NULL UNIQUE DEFAULT 1,

  /** Full legal name of the customer */
  Name: String(200) NOT NULL,

  /** Primary contact email address */
  Email: String(200) UNIQUE
);

Documentation comments can be placed before:

  • Entity definitions (becomes entity documentation)
  • Attribute definitions (becomes attribute documentation)
  • Enumeration definitions (becomes enumeration documentation)
  • Association definitions (becomes association documentation)

Updating Documentation

You can also set documentation on existing entities with ALTER ENTITY:

ALTER ENTITY Sales.Customer
  SET DOCUMENTATION 'Customer master data for the Sales module';

Script Files

MDL statements can be saved in .mdl files and executed as scripts. This is the primary way to automate Mendix model changes.

File Format

  • Extension: .mdl
  • Encoding: UTF-8
  • Statement termination: Semicolons (;) or forward slash (/) on its own line

A typical script file:

-- setup_domain_model.mdl
-- Creates the Sales domain model

CREATE ENUMERATION Sales.OrderStatus (
  Draft 'Draft',
  Pending 'Pending',
  Confirmed 'Confirmed',
  Shipped 'Shipped'
);

CREATE PERSISTENT ENTITY Sales.Customer (
  Name: String(200) NOT NULL,
  Email: String(200) UNIQUE
);

CREATE PERSISTENT ENTITY Sales.Order (
  OrderDate: DateTime NOT NULL,
  Status: Enumeration(Sales.OrderStatus) DEFAULT 'Draft'
);

CREATE ASSOCIATION Sales.Order_Customer
  FROM Sales.Customer
  TO Sales.Order
  TYPE Reference;

Executing Scripts

From the Command Line

Use mxcli exec to run a script against a project:

mxcli exec setup_domain_model.mdl -p /path/to/app.mpr

From the REPL

Use EXECUTE SCRIPT inside an interactive session:

CONNECT LOCAL '/path/to/app.mpr';
EXECUTE SCRIPT './scripts/setup_domain_model.mdl';

Syntax Checking

You can validate a script without connecting to a project:

# Syntax only
mxcli check script.mdl

# Syntax + reference validation (requires project)
mxcli check script.mdl -p app.mpr --references

Syntax checking catches parse errors, unknown keywords, and common anti-patterns before execution.

Comments in Scripts

Scripts support the same comment syntax as interactive MDL:

-- Single-line comment
/* Multi-line comment */
/** Documentation comment (attached to next element) */

Data Types

MDL has a type system that maps to the Mendix metamodel attribute types. Every attribute in an entity definition has a type that determines how values are stored and validated.

Type Categories

CategoryTypes
TextString, String(n), HashedString
NumericInteger, Long, Decimal, AutoNumber
LogicalBoolean
TemporalDateTime, Date
BinaryBinary
ReferenceEnumeration(Module.Name)

Quick Example

CREATE PERSISTENT ENTITY Demo.AllTypes (
  Id: AutoNumber NOT NULL UNIQUE DEFAULT 1,
  Code: String(10) NOT NULL UNIQUE,
  Name: String(200) NOT NULL,
  Description: String(unlimited),
  Counter: Integer DEFAULT 0,
  BigNumber: Long,
  Amount: Decimal DEFAULT 0.00,
  IsActive: Boolean DEFAULT TRUE,
  CreatedAt: DateTime,
  BirthDate: Date,
  Attachment: Binary,
  Status: Enumeration(Demo.Status) DEFAULT 'Active'
);

No Implicit Conversions

MDL does not perform implicit type conversions. Types must match exactly when assigning defaults or modifying attributes.

Compatible type changes (when using CREATE OR MODIFY):

  • String(100) 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

Primitive Types

This page documents each primitive type available for entity attributes in MDL.

String

Variable-length text data.

String              -- Default length (200)
String(n)           -- Specific length (1 to unlimited)
String(unlimited)   -- No length limit

Examples:

Name: String(200)
Description: String(unlimited)
Code: String(10)

Default value format:

Name: String(200) DEFAULT 'Unknown'
Code: String(10) DEFAULT ''

Integer

32-bit signed integer.

Integer

Range: -2,147,483,648 to 2,147,483,647

Quantity: Integer
Age: Integer DEFAULT 0
Priority: Integer NOT NULL DEFAULT 1

Long

64-bit signed integer.

Long

Range: -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807

FileSize: Long
TotalCount: Long DEFAULT 0

Decimal

High-precision decimal number with up to 20 digits total.

Decimal
Price: Decimal
Amount: Decimal DEFAULT 0
TaxRate: Decimal DEFAULT 0.21

Boolean

True/false value. Boolean attributes must have a DEFAULT value (enforced by Mendix Studio Pro).

Boolean DEFAULT TRUE
Boolean DEFAULT FALSE
IsActive: Boolean DEFAULT TRUE
Enabled: Boolean DEFAULT TRUE
Deleted: Boolean DEFAULT FALSE

DateTime

Date and time stored as a UTC timestamp.

DateTime
CreatedAt: DateTime
ModifiedAt: DateTime
ScheduledFor: DateTime

DateTime values include both date and time components. For date-only display, use Date instead.

Date

Date only (no time component). Internally stored as DateTime in Mendix, but the UI only shows the date portion.

Date
BirthDate: Date
ExpiryDate: Date

AutoNumber

Auto-incrementing integer, typically used for human-readable identifiers. The DEFAULT value specifies the starting number.

AutoNumber
OrderId: AutoNumber NOT NULL UNIQUE DEFAULT 1
CustomerId: AutoNumber

AutoNumber attributes automatically receive the next value on object creation. They are typically combined with NOT NULL and UNIQUE constraints.

Binary

Binary data for files, images, and other non-text content.

Binary
ProfileImage: Binary
Document: Binary
Thumbnail: Binary

File metadata (name, size, MIME type) is stored separately by Mendix. Maximum size is configurable per attribute.

HashedString

Securely hashed string, used for password storage. Values are one-way hashed and cannot be retrieved – comparison is done by hashing the input and comparing hashes.

HashedString
Password: HashedString

Enumeration Reference

References an enumeration type defined with CREATE ENUMERATION. See Enumerations for details.

Enumeration(<Module.EnumerationName>)
Status: Enumeration(Sales.OrderStatus)
Priority: Enumeration(Core.Priority) DEFAULT Core.Priority.Normal
Type: Enumeration(MyModule.ItemType) NOT NULL

Default value format:

-- Fully qualified (preferred)
DEFAULT Module.EnumName.ValueName

-- Legacy string literal form
DEFAULT 'ValueName'

See Also

Constraints

Constraints restrict the values an attribute can hold. They are specified after the type in an attribute definition.

Syntax

<name>: <type> [NOT NULL [ERROR '<message>']] [UNIQUE [ERROR '<message>']] [DEFAULT <value>]

Constraints must appear in this order:

  1. NOT NULL (optionally with ERROR)
  2. UNIQUE (optionally with ERROR)
  3. DEFAULT

NOT NULL

Marks the attribute as required. The value cannot be empty.

Name: String(200) NOT NULL

With a custom error message displayed to the user:

Name: String(200) NOT NULL ERROR 'Name is required'

UNIQUE

Enforces that the value must be unique across all objects of the entity.

Email: String(200) UNIQUE

With a custom error message:

Email: String(200) UNIQUE ERROR 'Email already exists'

DEFAULT

Sets the initial value when a new object is created.

Count: Integer DEFAULT 0
IsActive: Boolean DEFAULT TRUE
Status: Enumeration(Sales.OrderStatus) DEFAULT 'Draft'
Name: String(200) DEFAULT 'Unknown'

Default value syntax varies by type:

TypeDefault SyntaxExamples
StringDEFAULT 'value'DEFAULT '', DEFAULT 'Unknown'
IntegerDEFAULT nDEFAULT 0, DEFAULT -1
LongDEFAULT nDEFAULT 0
DecimalDEFAULT n.nDEFAULT 0, DEFAULT 0.00, DEFAULT 99.99
BooleanDEFAULT TRUE/FALSEDEFAULT TRUE, DEFAULT FALSE
AutoNumberDEFAULT nDEFAULT 1 (starting value)
EnumerationDEFAULT Module.Enum.ValueDEFAULT Shop.Status.Active, DEFAULT 'Pending'

Omitting the DEFAULT clause means no default value is set:

OptionalField: String(200)

Combining Constraints

All three constraints can be used together:

Email: String(200) NOT NULL ERROR 'Email is required'
                   UNIQUE ERROR 'Email already exists'
                   DEFAULT ''

More examples:

CREATE PERSISTENT ENTITY Sales.Product (
  -- Required only
  Name: String(200) NOT NULL,

  -- Required with custom error
  SKU: String(50) NOT NULL ERROR 'SKU is required for all products',

  -- Unique only
  Barcode: String(50) UNIQUE,

  -- Required and unique with custom errors
  ProductCode: String(20) NOT NULL ERROR 'Product code required'
                          UNIQUE ERROR 'Product code must be unique',

  -- Default only
  Quantity: Integer DEFAULT 0,

  -- No constraints
  Description: String(unlimited)
);

Validation Rule Mapping

Under the hood, constraints map to Mendix validation rules:

MDL ConstraintMendix Validation Rule
NOT NULLDomainModels$RequiredRuleInfo
UNIQUEDomainModels$UniqueRuleInfo

See Also

Enumerations

Enumerations define a fixed set of named values. They are used as attribute types to restrict a field to a specific list of options.

CREATE ENUMERATION

[/** <documentation> */]
CREATE ENUMERATION <Module>.<Name> (
  <value-name> '<caption>' [, ...]
)

Each value has an identifier (used in code) and a caption (displayed in the UI):

/** Order status enumeration */
CREATE ENUMERATION Sales.OrderStatus (
  Draft 'Draft',
  Pending 'Pending Approval',
  Approved 'Approved',
  Shipped 'Shipped',
  Delivered 'Delivered',
  Cancelled 'Cancelled'
);

Using Enumerations in Attributes

Reference an enumeration type in an attribute definition with Enumeration(Module.Name):

CREATE PERSISTENT ENTITY Sales.Order (
  Status: Enumeration(Sales.OrderStatus) DEFAULT 'Draft',
  Priority: Enumeration(Core.Priority) NOT NULL
);

The default value is the name of the enumeration value (not the caption). The fully qualified form is preferred:

-- Fully qualified (preferred)
Status: Enumeration(Sales.OrderStatus) DEFAULT Sales.OrderStatus.Draft

-- Legacy string literal form
Status: Enumeration(Sales.OrderStatus) DEFAULT 'Draft'

ALTER ENUMERATION

Add or remove values from an existing enumeration:

-- Add a new value
ALTER ENUMERATION Sales.OrderStatus
  ADD VALUE OnHold 'On Hold';

-- Remove a value
ALTER ENUMERATION Sales.OrderStatus
  REMOVE VALUE Draft;

DROP ENUMERATION

Remove an enumeration entirely:

DROP ENUMERATION Sales.OrderStatus;

Dropping an enumeration that is still referenced by entity attributes will cause errors in Studio Pro.

See Also

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 TypeBSON $TypeGo SDK Type
StringDomainModels$StringAttributeType*StringAttributeType
String(n)DomainModels$StringAttributeType + Length*StringAttributeType{Length: n}
IntegerDomainModels$IntegerAttributeType*IntegerAttributeType
LongDomainModels$LongAttributeType*LongAttributeType
DecimalDomainModels$DecimalAttributeType*DecimalAttributeType
BooleanDomainModels$BooleanAttributeType*BooleanAttributeType
DateTimeDomainModels$DateTimeAttributeType*DateTimeAttributeType
DateDomainModels$DateTimeAttributeType*DateTimeAttributeType
AutoNumberDomainModels$AutoNumberAttributeType*AutoNumberAttributeType
BinaryDomainModels$BinaryAttributeType*BinaryAttributeType
EnumerationDomainModels$EnumerationAttributeType*EnumerationAttributeType
HashedStringDomainModels$HashedStringAttributeType*HashedStringAttributeType

Note that Date and DateTime both map to the same underlying BSON type. The distinction is handled at the UI layer.

Default Value Mapping

Default values are stored in BSON as StoredValue structures:

MDL DefaultBSON Structure
DEFAULT 'text'Value: {$Type: "DomainModels$StoredValue", DefaultValue: "text"}
DEFAULT 123Value: {$Type: "DomainModels$StoredValue", DefaultValue: "123"}
DEFAULT TRUEValue: {$Type: "DomainModels$StoredValue", DefaultValue: "true"}
(calculated)Value: {$Type: "DomainModels$CalculatedValue", Microflow: <id>}

All default values are serialized as strings in the BSON storage, regardless of the attribute type.

See Also

Domain Model

The domain model is the data layer of a Mendix application. It defines the entities (data tables), their attributes (columns), relationships between entities (associations), and inheritance hierarchies (generalizations).

Core Concepts

ConceptMDL StatementDescription
EntityCREATE ENTITYA data structure, similar to a database table
AttributeDefined inside entityA field on an entity, similar to a column
AssociationCREATE ASSOCIATIONA relationship between two entities
GeneralizationEXTENDSInheritance – one entity extends another
IndexINDEX (...)Performance optimization for queries
EnumerationCREATE ENUMERATIONA fixed set of named values for an attribute

Module Scope

Every domain model element belongs to a module. Elements are always referenced by their qualified name in Module.Element format:

Sales.Customer           -- entity
Sales.OrderStatus        -- enumeration
Sales.Order_Customer     -- association

Complete Example

This example creates a small Sales domain model with related entities, an enumeration, and an association:

-- Enumeration for order statuses
CREATE ENUMERATION Sales.OrderStatus (
  Draft 'Draft',
  Pending 'Pending',
  Confirmed 'Confirmed',
  Shipped 'Shipped',
  Delivered 'Delivered',
  Cancelled 'Cancelled'
);

-- Customer entity
/** Customer master data */
@Position(100, 100)
CREATE PERSISTENT ENTITY Sales.Customer (
  CustomerId: AutoNumber NOT NULL UNIQUE DEFAULT 1,
  Name: String(200) NOT NULL ERROR 'Customer name is required',
  Email: String(200) UNIQUE ERROR 'Email already registered',
  Phone: String(50),
  IsActive: Boolean DEFAULT TRUE,
  CreatedAt: DateTime
)
INDEX (Name)
INDEX (Email);

-- Order entity
/** Sales order */
@Position(300, 100)
CREATE PERSISTENT ENTITY Sales.Order (
  OrderId: AutoNumber NOT NULL UNIQUE DEFAULT 1,
  OrderNumber: String(50) NOT NULL UNIQUE,
  OrderDate: DateTime NOT NULL,
  TotalAmount: Decimal DEFAULT 0,
  Status: Enumeration(Sales.OrderStatus) DEFAULT 'Draft',
  Notes: String(unlimited)
)
INDEX (OrderNumber)
INDEX (OrderDate DESC);

-- Association: Order belongs to Customer
CREATE ASSOCIATION Sales.Order_Customer
  FROM Sales.Customer
  TO Sales.Order
  TYPE Reference
  OWNER Default
  DELETE_BEHAVIOR DELETE_BUT_KEEP_REFERENCES;

Further Reading

Entities

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

TypeMDL KeywordDescription
PersistentPERSISTENTStored in the database with a corresponding table
Non-PersistentNON-PERSISTENTIn-memory only, scoped to the user session
ViewVIEWBased on an OQL query, read-only
ExternalEXTERNALFrom an external data source (OData, etc.)

CREATE ENTITY

[/** <documentation> */]
[@Position(<x>, <y>)]
CREATE [OR MODIFY] <entity-type> ENTITY <Module>.<Name> (
  <attribute-definitions>
)
[INDEX (<column-list>)]
[;|/]

Persistent Entity

The most common type. Data is stored in the application database:

/** Customer entity */
@Position(100, 200)
CREATE PERSISTENT ENTITY Sales.Customer (
  CustomerId: AutoNumber NOT NULL UNIQUE DEFAULT 1,
  Name: String(200) NOT NULL ERROR 'Name is required',
  Email: String(200) UNIQUE ERROR 'Email must be unique',
  Balance: Decimal DEFAULT 0,
  IsActive: Boolean DEFAULT TRUE,
  CreatedDate: DateTime,
  Status: Enumeration(Sales.CustomerStatus) DEFAULT 'Active'
)
INDEX (Name)
INDEX (Email);

Non-Persistent Entity

Used for helper objects, filter parameters, and UI state that does not need database storage:

CREATE NON-PERSISTENT ENTITY Sales.CustomerFilter (
  SearchName: String(200),
  MinBalance: Decimal,
  MaxBalance: Decimal
);

View Entity

Defined by an OQL query. View entities are read-only:

CREATE VIEW ENTITY Reports.CustomerSummary (
  CustomerName: String,
  TotalOrders: Integer,
  TotalAmount: Decimal
) AS
  SELECT
    c.Name AS CustomerName,
    COUNT(o.OrderId) AS TotalOrders,
    SUM(o.Amount) AS TotalAmount
  FROM Sales.Customer c
  LEFT JOIN Sales.Order o ON o.Customer = c
  GROUP BY c.Name;

CREATE OR MODIFY

Creates the entity if it does not exist, or updates it if it does. New attributes are added; existing attributes are preserved:

CREATE OR MODIFY PERSISTENT ENTITY Sales.Customer (
  CustomerId: AutoNumber NOT NULL UNIQUE,
  Name: String(200) NOT NULL,
  Email: String(200),
  Phone: String(50)  -- new attribute added on modify
);

Annotations

@Position

Controls where the entity appears in the domain model diagram:

@Position(100, 200)
CREATE PERSISTENT ENTITY Sales.Customer ( ... );

Documentation

A /** ... */ comment before the entity becomes its documentation in Studio Pro:

/** Customer master data.
 *  Stores both active and inactive customers.
 */
CREATE PERSISTENT ENTITY Sales.Customer ( ... );

DROP ENTITY

Removes an entity from the domain model:

DROP ENTITY Sales.Customer;

See Also

Attributes and Validation Rules

Attributes define the fields on an entity. Each attribute has a name, a type, and optional constraints.

Attribute Syntax

[/** <documentation> */]
<name>: <type> [NOT NULL [ERROR '<message>']] [UNIQUE [ERROR '<message>']] [DEFAULT <value>]
ComponentDescription
NameAttribute identifier (follows identifier rules)
TypeOne of the primitive types or an enumeration reference
NOT NULLValue is required
UNIQUEValue must be unique across all objects
DEFAULTInitial value on object creation
/** ... */Documentation comment

Attribute Ordering

Attributes are listed in order, separated by commas. The last attribute has no trailing comma:

CREATE PERSISTENT ENTITY Module.Entity (
  FirstAttr: String(200),      -- comma after
  SecondAttr: Integer,         -- comma after
  LastAttr: Boolean DEFAULT FALSE  -- no comma
);

Validation Rules

Validation rules are expressed as attribute constraints. When validation fails, Mendix displays the error message (if provided) or a default message.

ValidationMDL SyntaxDescription
RequiredNOT NULLAttribute must have a value
Required with messageNOT NULL ERROR 'message'Custom error message on empty
UniqueUNIQUEValue must be unique across all objects
Unique with messageUNIQUE ERROR 'message'Custom error message on duplicate

Example with Validation

CREATE PERSISTENT ENTITY Sales.Product (
  -- Required only
  Name: String(200) NOT NULL,

  -- Required with custom error
  SKU: String(50) NOT NULL ERROR 'SKU is required for all products',

  -- Unique only
  Barcode: String(50) UNIQUE,

  -- Required and unique with custom errors
  ProductCode: String(20) NOT NULL ERROR 'Product code required'
                          UNIQUE ERROR 'Product code must be unique',

  -- Optional field (no validation)
  Description: String(unlimited)
);

Documentation Comments

Attach documentation to individual attributes with /** ... */:

CREATE PERSISTENT ENTITY Sales.Customer (
  /** Unique customer identifier, auto-generated */
  CustomerId: AutoNumber NOT NULL UNIQUE DEFAULT 1,

  /** Full legal name of the customer */
  Name: String(200) NOT NULL,

  /** Primary contact email address */
  Email: String(200) UNIQUE
);

CALCULATED Attributes

An attribute can be marked as calculated, meaning its value is computed by a microflow rather than stored:

FullName: String(400) CALCULATED

Calculated attributes use a CALCULATED BY clause to specify the source microflow. See the MDL quick reference for details.

See Also

Associations

Associations define relationships between entities. They determine how objects reference each other and control behavior on deletion.

Association Types

TypeMDL KeywordCardinalityDescription
ReferenceReferenceMany-to-OneMany objects on the FROM side reference one object on the TO side
ReferenceSetReferenceSetMany-to-ManyObjects on both sides can reference multiple objects

CREATE ASSOCIATION

[/** <documentation> */]
CREATE ASSOCIATION <Module>.<Name>
  FROM <ParentEntity>
  TO <ChildEntity>
  TYPE <Reference|ReferenceSet>
  [OWNER <Default|Both|Parent|Child>]
  [DELETE_BEHAVIOR <behavior>]

Reference (Many-to-One)

A Reference association means each object on the FROM side can reference one object on the TO side. Multiple FROM objects can reference the same TO object.

/** Order belongs to Customer (many-to-one) */
CREATE ASSOCIATION Sales.Order_Customer
  FROM Sales.Customer
  TO Sales.Order
  TYPE Reference
  OWNER Default
  DELETE_BEHAVIOR DELETE_BUT_KEEP_REFERENCES;

ReferenceSet (Many-to-Many)

A ReferenceSet association allows multiple objects on both sides to reference each other:

/** Order has many Products (many-to-many) */
CREATE ASSOCIATION Sales.Order_Product
  FROM Sales.Order
  TO Sales.Product
  TYPE ReferenceSet
  OWNER Both;

Owner Options

The owner determines which side of the association can modify the reference.

OwnerDescription
DefaultChild (FROM side) owns the association
BothBoth sides can modify the association
ParentOnly parent (TO side) can modify
ChildOnly child (FROM side) can modify

Delete Behavior

Controls what happens when an associated object is deleted.

BehaviorMDL KeywordDescription
Keep referencesDELETE_BUT_KEEP_REFERENCESDelete the object, set references to null
CascadeDELETE_CASCADEDelete associated objects as well
/** Invoice must be deleted with Order */
CREATE ASSOCIATION Sales.Order_Invoice
  FROM Sales.Order
  TO Sales.Invoice
  TYPE Reference
  DELETE_BEHAVIOR DELETE_CASCADE;

Naming Convention

Association names typically follow the pattern Module.FromEntity_ToEntity:

Sales.Order_Customer       -- Order references Customer
Sales.Order_Product        -- Order references Product
Sales.Order_Invoice        -- Order references Invoice

DROP ASSOCIATION

Remove an association:

DROP ASSOCIATION Sales.Order_Customer;

See Also

  • Domain Model – overview of domain model concepts
  • Entities – the entities that associations connect
  • Generalization – inheritance (a different kind of relationship)

Generalization

Generalization (inheritance) allows an entity to extend another entity, inheriting all of its attributes and associations. The child entity can add its own attributes on top.

Syntax

CREATE PERSISTENT ENTITY <Module>.<Name>
  EXTENDS <ParentEntity>
(
  <additional-attributes>
);

Both EXTENDS (preferred) and GENERALIZATION (legacy) keywords are supported.

Examples

Extending System.User

The most common use of generalization is creating an application-specific user entity:

/** Employee extends User with additional fields */
CREATE PERSISTENT ENTITY HR.Employee EXTENDS System.User (
  EmployeeNumber: String(20) NOT NULL UNIQUE,
  Department: String(100),
  HireDate: Date
);

The HR.Employee entity inherits all attributes from System.User (Name, Password, etc.) and adds EmployeeNumber, Department, and HireDate.

Extending System.Image

For entities that store images:

/** Product photo entity */
CREATE PERSISTENT ENTITY Catalog.ProductPhoto EXTENDS System.Image (
  Caption: String(200),
  SortOrder: Integer DEFAULT 0
);

Extending System.FileDocument

For entities that store file attachments:

/** File attachment entity */
CREATE PERSISTENT ENTITY Docs.Attachment EXTENDS System.FileDocument (
  Description: String(500)
);

Common System Generalizations

Parent EntityPurpose
System.UserUser accounts with authentication
System.FileDocumentFile storage (name, size, content)
System.ImageImage storage (extends FileDocument with dimensions)

Inheritance Rules

  • A persistent entity can extend another persistent entity
  • The child entity inherits all attributes and associations of the parent
  • The child entity can add new attributes but cannot remove inherited ones
  • Associations to the parent entity also apply to child objects
  • Database queries on the parent entity include child objects

See Also

Indexes

Indexes improve query performance for frequently searched or sorted attributes. They are defined after the attribute list in an entity definition.

Syntax

INDEX (<column> [ASC|DESC] [, <column> [ASC|DESC] ...])

Indexes are placed after the closing parenthesis of the attribute list:

CREATE PERSISTENT ENTITY Sales.Order (
  OrderId: AutoNumber NOT NULL UNIQUE,
  OrderNumber: String(50) NOT NULL,
  CustomerId: Long,
  OrderDate: DateTime,
  Status: Enumeration(Sales.OrderStatus)
)
-- Single column index
INDEX (OrderNumber)
-- Composite index with sort direction
INDEX (CustomerId, OrderDate DESC)
-- Another single column index
INDEX (Status);

Sort Direction

Each column in an index can specify a sort direction:

DirectionKeywordDefault
AscendingASCYes (default)
DescendingDESCNo
INDEX (Name)                    -- ascending (default)
INDEX (Name ASC)                -- explicit ascending
INDEX (CreatedAt DESC)          -- descending
INDEX (Name, CreatedAt DESC)    -- mixed: Name ascending, CreatedAt descending

Adding and Removing Indexes

Use ALTER ENTITY to add or remove indexes on existing entities:

-- Add an index
ALTER ENTITY Sales.Customer
  ADD INDEX (Email);

-- Remove an index
ALTER ENTITY Sales.Customer
  DROP INDEX (Email);

Guidelines

  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

  • Entities – entity definitions that contain indexes
  • ALTER ENTITY – adding/removing indexes on existing entities

ALTER ENTITY

ALTER ENTITY modifies an existing entity’s attributes, indexes, or documentation without recreating it. This is useful for incremental changes to entities that already contain data.

ADD Attributes

Add one or more new attributes:

ALTER ENTITY Sales.Customer
  ADD (Phone: String(50), Notes: String(unlimited));

New attributes support the same constraints as in CREATE ENTITY:

ALTER ENTITY Sales.Customer
  ADD (
    LoyaltyPoints: Integer DEFAULT 0,
    MemberSince: DateTime NOT NULL
  );

DROP Attributes

Remove one or more attributes:

ALTER ENTITY Sales.Customer
  DROP (Notes);

Multiple attributes can be dropped at once:

ALTER ENTITY Sales.Customer
  DROP (Notes, TempField, OldStatus);

MODIFY Attributes

Change the type or constraints of existing attributes:

ALTER ENTITY Sales.Customer
  MODIFY (Name: String(400) NOT NULL);

RENAME Attributes

Rename an attribute:

ALTER ENTITY Sales.Customer
  RENAME Phone TO PhoneNumber;

ADD INDEX

Add an index to the entity:

ALTER ENTITY Sales.Customer
  ADD INDEX (Email);

Composite indexes:

ALTER ENTITY Sales.Customer
  ADD INDEX (Name, CreatedAt DESC);

DROP INDEX

Remove an index:

ALTER ENTITY Sales.Customer
  DROP INDEX (Email);

SET DOCUMENTATION

Update the entity’s documentation text:

ALTER ENTITY Sales.Customer
  SET DOCUMENTATION 'Customer master data for the Sales module';

Syntax Summary

ALTER ENTITY <Module>.<Entity>
  ADD (<attribute-definition> [, ...])

ALTER ENTITY <Module>.<Entity>
  DROP (<attribute-name> [, ...])

ALTER ENTITY <Module>.<Entity>
  MODIFY (<attribute-definition> [, ...])

ALTER ENTITY <Module>.<Entity>
  RENAME <old-name> TO <new-name>

ALTER ENTITY <Module>.<Entity>
  ADD INDEX (<column-list>)

ALTER ENTITY <Module>.<Entity>
  DROP INDEX (<column-list>)

ALTER ENTITY <Module>.<Entity>
  SET DOCUMENTATION '<text>'

See Also

Microflows and Nanoflows

Microflows and nanoflows are the primary logic constructs in Mendix applications. They define executable sequences of activities – retrieving data, creating objects, calling services, showing pages, and more. In MDL, you create and manage them with declarative SQL-like syntax.

What is a Microflow?

A microflow is a server-side logic flow. It executes on the Mendix runtime, has access to the database, can call external services, and supports transactions with error handling. Microflows are the workhorse of Mendix application logic.

Typical uses:

  • CRUD operations (create, read, update, delete objects)
  • Validation logic before saving
  • Calling external web services or Java actions
  • Batch processing and data transformations
  • Scheduled event handlers

What is a Nanoflow?

A nanoflow is a client-side logic flow. It executes in the user’s browser (or on mobile devices), providing fast response times without server round-trips. Nanoflows have a more limited set of available activities – they cannot access the database directly or use Java actions.

Typical uses:

  • Client-side validation
  • Showing/closing pages
  • Changing objects already in memory
  • Calling nanoflows or microflows

Basic Syntax

Both microflows and nanoflows follow the same structural pattern in MDL:

CREATE MICROFLOW Module.ActionName
FOLDER 'OptionalFolder'
BEGIN
  -- parameters, variables, activities, return
END;
CREATE NANOFLOW Module.ClientAction
BEGIN
  -- activities (client-side subset)
END;

SHOW and DESCRIBE

List microflows and nanoflows in the project:

SHOW MICROFLOWS
SHOW MICROFLOWS IN MyModule
SHOW NANOFLOWS
SHOW NANOFLOWS IN MyModule

View the full MDL definition of an existing microflow or nanoflow (round-trippable output):

DESCRIBE MICROFLOW MyModule.ACT_CreateOrder
DESCRIBE NANOFLOW MyModule.NAV_ShowDetails

DROP

Remove a microflow or nanoflow:

DROP MICROFLOW MyModule.ACT_CreateOrder;
DROP NANOFLOW MyModule.NAV_ShowDetails;

OR REPLACE

Use CREATE OR REPLACE to overwrite an existing microflow or nanoflow if it already exists, or create it if it does not:

CREATE OR REPLACE MICROFLOW MyModule.ACT_CreateOrder
BEGIN
  -- updated logic
END;

Folder Organization

Place microflows and nanoflows into folders for project organization:

CREATE MICROFLOW Sales.ACT_CreateOrder
FOLDER 'Orders'
BEGIN
  -- ...
END;

Move an existing microflow to a different folder:

MOVE MICROFLOW Sales.ACT_CreateOrder TO FOLDER 'Orders/Actions';
MOVE NANOFLOW Sales.NAV_ShowDetail TO FOLDER 'Navigation';

Nested folders use / as the separator. Missing folders are created automatically.

Quick Example

CREATE MICROFLOW Sales.ACT_CreateOrder
FOLDER 'Orders'
BEGIN
  DECLARE $Order Sales.Order;
  $Order = CREATE Sales.Order (
    OrderDate = [%CurrentDateTime%],
    Status = 'Draft'
  );
  COMMIT $Order;
  SHOW PAGE Sales.Order_Edit ($Order = $Order);
  RETURN $Order;
END;

This microflow creates a new Order object with default values, commits it to the database, opens the edit page, and returns the new order.

Next Steps

  • Structure – parameters, variables, return types, and the full CREATE MICROFLOW syntax
  • Activity Types – all available activities (RETRIEVE, CREATE, CHANGE, COMMIT, DELETE, CALL, LOG, etc.)
  • Control Flow – IF/ELSE, LOOP, WHILE, error handling
  • Expressions – expression syntax for conditions and calculations
  • Nanoflows vs Microflows – differences and nanoflow-specific syntax
  • Common Patterns – CRUD, validation, batch processing, and anti-patterns to avoid

Structure

This page covers the structural elements of a microflow definition: the CREATE MICROFLOW syntax, parameters, variable declarations, return values, and annotations.

Full CREATE MICROFLOW Syntax

CREATE [OR REPLACE] MICROFLOW <Module.Name>
  [FOLDER '<path>']
BEGIN
  [<declarations>]
  [<activities>]
  [RETURN <value>;]
END;

The OR REPLACE modifier overwrites an existing microflow of the same name. The FOLDER clause organizes the microflow within the module’s folder structure.

Parameters

Parameters are declared with DECLARE at the top of the BEGIN...END block. They define the inputs to the microflow.

Primitive Parameters

DECLARE $Name String;
DECLARE $Count Integer;
DECLARE $IsActive Boolean;
DECLARE $Amount Decimal;
DECLARE $StartDate DateTime;

Supported primitive types: String, Integer, Long, Boolean, Decimal, DateTime.

Entity Parameters

Entity parameters receive a single object:

DECLARE $Customer MyModule.Customer;

Important: Do not use = empty or AS with entity declarations. The correct syntax is simply DECLARE $Var Module.Entity;.

List Parameters

List parameters receive a list of objects:

DECLARE $Orders List of Sales.Order = empty;

The = empty initializer creates an empty list. This is required for list declarations.

Parameter vs. Local Variable

In MDL, all DECLARE statements at the top of a microflow are treated as parameters. Variables created by activities (such as $Var = CREATE ... or RETRIEVE $Var ...) are local variables. If you need a local variable with a default value, use DECLARE followed by SET:

DECLARE $Counter Integer = 0;
SET $Counter = 10;

Variables and Assignment

Variable Declaration

DECLARE $Message String = 'Hello';
DECLARE $Total Decimal = 0;
DECLARE $Found Boolean = false;

Assignment with SET

Change the value of an already-declared variable:

SET $Counter = $Counter + 1;
SET $FullName = $FirstName + ' ' + $LastName;
SET $IsValid = $Amount > 0;

The variable must be declared before it can be assigned.

Variables from Activities

Activities like CREATE and RETRIEVE produce result variables:

$Order = CREATE Sales.Order (Status = 'New');
RETRIEVE $Customer FROM Sales.Customer WHERE Email = $Email LIMIT 1;
$Result = CALL MICROFLOW Sales.CalculateTotal (Order = $Order);

Return Values

Every flow path should end with a RETURN statement. The return type is inferred from the returned value.

Returning a Primitive

RETURN true;
RETURN $Total;
RETURN 'Success';

Returning an Object

RETURN $Order;

Returning a List

RETURN $FilteredOrders;

Returning Nothing

If the microflow returns nothing (void), you can omit the RETURN or use:

RETURN;

Annotations

Annotations are metadata decorators placed before an activity. They control visual layout and documentation in Mendix Studio Pro.

Position

Set the canvas position of the next activity:

@position(200, 100)
$Order = CREATE Sales.Order (Status = 'New');

Caption

Set a custom caption displayed on the activity in the canvas:

@caption 'Create new order'
$Order = CREATE Sales.Order (Status = 'New');

Color

Set the background color of the activity:

@color Green
COMMIT $Order;

Annotation (Visual Note)

Attach a visual annotation note to the next activity:

@annotation 'This step validates the input before saving'
VALIDATION FEEDBACK $Customer/Email MESSAGE 'Email is required';

Combining Annotations

Multiple annotations can be stacked before a single activity:

@position(300, 200)
@caption 'Validate and save'
@color Green
@annotation 'Final step: commit the validated order'
COMMIT $Order WITH EVENTS;

Complete Example

CREATE MICROFLOW Sales.ACT_ProcessOrder
FOLDER 'Orders/Processing'
BEGIN
  -- Parameters
  DECLARE $Order Sales.Order;
  DECLARE $ApplyDiscount Boolean;

  -- Local variable
  DECLARE $Total Decimal = 0;

  -- Retrieve order lines
  RETRIEVE $Lines FROM $Order/Sales.OrderLine_Order;

  -- Calculate total
  @caption 'Calculate total'
  $Total = CALL MICROFLOW Sales.SUB_CalculateTotal (
    OrderLines = $Lines
  );

  -- Apply discount if requested
  IF $ApplyDiscount THEN
    SET $Total = $Total * 0.9;
  END IF;

  -- Update order
  CHANGE $Order (
    TotalAmount = $Total,
    Status = 'Processed'
  );
  COMMIT $Order;

  RETURN $Order;
END;

Activity Types

This page documents every activity type available in MDL microflows. Activities are the individual steps that make up a microflow’s logic.

Object Operations

CREATE

Creates a new object of the specified entity type and assigns attribute values:

$Order = CREATE Sales.Order (
  OrderDate = [%CurrentDateTime%],
  Status = 'Draft',
  TotalAmount = 0
);

Attribute assignments are comma-separated Name = value pairs. The result is assigned to a variable.

CHANGE

Modifies attributes of an existing object:

CHANGE $Order (
  Status = 'Confirmed',
  TotalAmount = $CalculatedTotal
);

Multiple attributes can be changed in a single CHANGE statement.

COMMIT

Persists an object (or its changes) to the database:

COMMIT $Order;

Options:

  • WITH EVENTS – triggers event handlers (before/after commit microflows)
  • REFRESH – refreshes the object in the client after committing
COMMIT $Order WITH EVENTS;
COMMIT $Order REFRESH;
COMMIT $Order WITH EVENTS REFRESH;

DELETE

Deletes an object from the database:

DELETE $Order;

ROLLBACK

Reverts uncommitted changes to an object, restoring it to its last committed state:

ROLLBACK $Order;
ROLLBACK $Order REFRESH;

The REFRESH option refreshes the object in the client.

Retrieval

RETRIEVE from Database (XPath)

Retrieves objects from the database using an optional XPath-style WHERE clause:

-- Retrieve a single object
RETRIEVE $Customer FROM Sales.Customer
  WHERE Email = $InputEmail
  LIMIT 1;

-- Retrieve a list of objects
RETRIEVE $ActiveOrders FROM Sales.Order
  WHERE Status = 'Active';

-- Retrieve all objects of an entity
RETRIEVE $AllProducts FROM Sales.Product;

-- Retrieve with multiple conditions
RETRIEVE $RecentOrders FROM Sales.Order
  WHERE Status = 'Active'
  AND OrderDate > [%BeginOfCurrentDay%]
  LIMIT 50;

When LIMIT 1 is specified, the result is a single entity object. Otherwise, the result is a list.

RETRIEVE by Association

Retrieves objects by following an association from an existing object:

-- Retrieve related objects via association
RETRIEVE $Lines FROM $Order/Sales.OrderLine_Order;

-- Retrieve a single associated object
RETRIEVE $Customer FROM $Order/Sales.Order_Customer;

The association path uses the format $Variable/Module.AssociationName.

RETRIEVE from Variable List

Retrieves from an in-memory list variable:

RETRIEVE $Match FROM $CustomerList
  WHERE Email = $SearchEmail
  LIMIT 1;

Call Activities

CALL MICROFLOW

Calls another microflow, passing parameters and optionally receiving a return value:

-- Call with return value
$Total = CALL MICROFLOW Sales.SUB_CalculateTotal (
  OrderLines = $Lines
);

-- Call without return value
CALL MICROFLOW Sales.SUB_SendNotification (
  Customer = $Customer,
  Message = 'Order confirmed'
);

-- Call with no parameters
$Config = CALL MICROFLOW Admin.GetSystemConfig ();

CALL NANOFLOW

Calls a nanoflow from within a microflow:

$Result = CALL NANOFLOW MyModule.ValidateInput (
  InputValue = $Value
);

CALL JAVA ACTION

Calls a Java action:

$Hash = CALL JAVA ACTION MyModule.HashPassword (
  Password = $RawPassword
);

Java action parameters follow the same Name = value syntax.

UI Activities

SHOW PAGE

Opens a page, passing parameters:

SHOW PAGE Sales.Order_Edit ($Order = $Order);

The parameter syntax uses $PageParam = $MicroflowVar. Multiple parameters are comma-separated:

SHOW PAGE Sales.OrderDetail (
  $Order = $Order,
  $Customer = $Customer
);

An alternate syntax using colon notation is also supported:

SHOW PAGE Sales.Order_Edit (Order: $Order);

CLOSE PAGE

Closes the current page:

CLOSE PAGE;

Validation

VALIDATION FEEDBACK

Displays a validation error message on a specific attribute of an object:

VALIDATION FEEDBACK $Customer/Email MESSAGE 'Email address is required';
VALIDATION FEEDBACK $Order/TotalAmount MESSAGE 'Total must be greater than zero';

The syntax is VALIDATION FEEDBACK $Variable/AttributeName MESSAGE 'message text'.

Logging

LOG

Writes a message to the Mendix runtime log:

LOG INFO 'Order created successfully';
LOG WARNING 'Customer has no email address';
LOG ERROR 'Failed to process payment';

Log levels: INFO, WARNING, ERROR.

Optionally specify a log node name:

LOG INFO NODE 'OrderProcessing' 'Order created: ' + $Order/OrderNumber;
LOG ERROR NODE 'PaymentGateway' 'Payment failed for order ' + $Order/OrderNumber;

Database Query Execution

EXECUTE DATABASE QUERY

Executes a Database Connector query defined in the project:

-- Basic execution (3-part qualified name: Module.Connection.Query)
$Result = EXECUTE DATABASE QUERY MyModule.MyConn.GetCustomers;

-- Dynamic SQL query
$Result = EXECUTE DATABASE QUERY MyModule.MyConn.SearchQuery
  DYNAMIC 'SELECT * FROM customers WHERE name LIKE ?';

-- With parameters
$Result = EXECUTE DATABASE QUERY MyModule.MyConn.GetByEmail
  PARAMETERS ($EmailParam = $Email);

-- With runtime connection override
$Result = EXECUTE DATABASE QUERY MyModule.MyConn.GetData
  CONNECTION $RuntimeConnString;

The query name follows a three-part naming convention: Module.ConnectionName.QueryName.

Note: Error handling (ON ERROR) is not supported on EXECUTE DATABASE QUERY activities.

Summary Table

ActivitySyntaxReturns
Create object$Var = CREATE Module.Entity (Attr = val);Entity object
Change objectCHANGE $Var (Attr = val);
CommitCOMMIT $Var [WITH EVENTS] [REFRESH];
DeleteDELETE $Var;
RollbackROLLBACK $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 pageSHOW PAGE Module.Page ($Param = $val);
Close pageCLOSE PAGE;
ValidationVALIDATION FEEDBACK $Var/Attr MESSAGE 'msg';
LogLOG INFO|WARNING|ERROR [NODE 'name'] 'msg';
DB query$Var = EXECUTE DATABASE QUERY Module.Conn.Query;Result set
AssignmentSET $Var = expression;

Control Flow

MDL microflows support conditional branching, loops, and error handling to control the execution path of your logic.

IF / ELSE

Conditional branching executes different activities based on a boolean expression.

Basic IF

IF $Order/TotalAmount > 1000 THEN
  CHANGE $Order (DiscountApplied = true);
END IF;

IF / ELSE

IF $Customer/Email != empty THEN
  CALL MICROFLOW Sales.SUB_SendEmail (Customer = $Customer);
ELSE
  LOG WARNING 'Customer has no email address';
END IF;

Nested IF

Since MDL does not support CASE/WHEN (switch statements), use nested IF...ELSE blocks:

IF $Order/Status = 'Draft' THEN
  CHANGE $Order (Status = 'Submitted');
ELSE
  IF $Order/Status = 'Submitted' THEN
    CHANGE $Order (Status = 'Approved');
  ELSE
    IF $Order/Status = 'Approved' THEN
      CHANGE $Order (Status = 'Shipped');
    ELSE
      LOG WARNING 'Unexpected order status: ' + $Order/Status;
    END IF;
  END IF;
END IF;

Complex Conditions

Conditions support AND, OR, and parentheses:

IF $Amount > 0 AND $Customer != empty THEN
  COMMIT $Order;
END IF;

IF ($Status = 'Active' OR $Status = 'Pending') AND $IsValid = true THEN
  CALL MICROFLOW Sales.ProcessOrder (Order = $Order);
END IF;

LOOP (FOR EACH)

Iterates over each item in a list:

LOOP $Line IN $OrderLines
BEGIN
  CHANGE $Line (
    LineTotal = $Line/Quantity * $Line/UnitPrice
  );
  COMMIT $Line;
END LOOP;

The loop variable ($Line) is automatically declared and takes the entity type of the list.

LOOP with Nested Logic

LOOP $Order IN $PendingOrders
BEGIN
  IF $Order/TotalAmount > 0 THEN
    CHANGE $Order (Status = 'Confirmed');
    COMMIT $Order;
  ELSE
    DELETE $Order;
  END IF;
END LOOP;

BREAK and CONTINUE

Use BREAK to exit a loop early, and CONTINUE to skip to the next iteration:

LOOP $Item IN $Items
BEGIN
  IF $Item/IsInvalid = true THEN
    CONTINUE;
  END IF;

  IF $Item/Type = 'StopSignal' THEN
    BREAK;
  END IF;

  CALL MICROFLOW Sales.ProcessItem (Item = $Item);
END LOOP;

WHILE Loop

Executes a block repeatedly as long as a condition remains true:

DECLARE $Counter Integer = 0;

WHILE $Counter < 10
BEGIN
  SET $Counter = $Counter + 1;
  LOG INFO 'Iteration: ' + toString($Counter);
END WHILE;

Caution: Ensure the condition will eventually become false to avoid infinite loops.

Error Handling

ON ERROR Suffix

Error handling is applied as a suffix to individual activities. There is no TRY...CATCH block in MDL. Instead, you specify error handling behavior on specific activities.

ON ERROR CONTINUE

Ignores the error and continues to the next activity:

COMMIT $Order ON ERROR CONTINUE;

ON ERROR ROLLBACK

Rolls back the current transaction and continues:

DELETE $Order ON ERROR ROLLBACK;

ON ERROR with Handler Block

Executes a custom error handling block when the activity fails:

COMMIT $Order ON ERROR {
  LOG ERROR 'Failed to commit order: ' + $Order/OrderNumber;
  ROLLBACK $Order;
};

The handler block can contain any activities – logging, rollback, showing validation messages, etc.

Error Handling Examples

-- Continue despite retrieval failure
RETRIEVE $Config FROM Admin.SystemConfig LIMIT 1 ON ERROR CONTINUE;

-- Custom error handler for external call
$Response = CALL MICROFLOW Integration.CallExternalAPI (
  Payload = $RequestBody
) ON ERROR {
  LOG ERROR NODE 'Integration' 'External API call failed';
  SET $Response = empty;
};

-- Rollback on commit failure
COMMIT $Order WITH EVENTS ON ERROR ROLLBACK;

Note: ON ERROR is not supported on EXECUTE DATABASE QUERY activities.

Unsupported Control Flow

The following constructs are not supported in MDL and will cause parse errors:

UnsupportedUse Instead
CASE ... WHEN ... END CASENested IF ... ELSE ... END IF
TRY ... CATCH ... END TRYON ERROR { ... } blocks on individual activities

Complete Example

CREATE MICROFLOW Sales.ACT_ProcessBatch
FOLDER 'Batch'
BEGIN
  DECLARE $Orders List of Sales.Order = empty;
  DECLARE $SuccessCount Integer = 0;
  DECLARE $ErrorCount Integer = 0;

  RETRIEVE $Orders FROM Sales.Order
    WHERE Status = 'Pending';

  LOOP $Order IN $Orders
  BEGIN
    IF $Order/TotalAmount <= 0 THEN
      LOG WARNING 'Skipping order with zero amount: ' + $Order/OrderNumber;
      CONTINUE;
    END IF;

    @caption 'Process order'
    CALL MICROFLOW Sales.SUB_ProcessSingleOrder (
      Order = $Order
    ) ON ERROR {
      LOG ERROR 'Failed to process order: ' + $Order/OrderNumber;
      SET $ErrorCount = $ErrorCount + 1;
      CONTINUE;
    };

    SET $SuccessCount = $SuccessCount + 1;
  END LOOP;

  LOG INFO 'Batch complete: ' + toString($SuccessCount) + ' processed, '
    + toString($ErrorCount) + ' errors';
END;

Expressions

Expressions in MDL are used in conditions, attribute assignments, variable assignments, and log messages within microflows. They follow Mendix expression syntax.

Literals

String Literals

Strings are enclosed in single quotes:

'Hello, world'
'Order #1234'

To include a single quote within a string, double it:

'it''s here'
'Customer''s address'

Important: Do not use backslash escaping (\'). Mendix expression syntax requires doubled single quotes.

Numeric Literals

42          -- Integer
3.14        -- Decimal
-100        -- Negative integer
1.5e10      -- Scientific notation

Boolean Literals

true
false

Empty

The empty literal represents a null/undefined value:

DECLARE $List List of Sales.Order = empty;

IF $Customer = empty THEN
  LOG WARNING 'No customer found';
END IF;

Operators

Arithmetic Operators

OperatorDescriptionExample
+Addition / string concatenation$Price + $Tax
-Subtraction$Total - $Discount
*Multiplication$Quantity * $UnitPrice
divDivision$Total div $Count
modModulo (remainder)$Index mod 2

String concatenation uses +:

SET $FullName = $FirstName + ' ' + $LastName;
LOG INFO 'Processing order: ' + $Order/OrderNumber;

Comparison Operators

OperatorDescriptionExample
=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

OperatorDescriptionExample
ANDLogical AND$IsActive AND $HasEmail
ORLogical OR$IsAdmin OR $IsManager
NOTLogical NOTNOT $IsDeleted

Parentheses control precedence:

IF ($Status = 'Active' OR $Status = 'Pending') AND $Amount > 0 THEN
  -- ...
END IF;

Attribute Access

Access attributes of an object variable with /:

$Order/TotalAmount
$Customer/Email
$Line/Quantity

This is used in conditions, assignments, and expressions:

IF $Order/TotalAmount > 1000 THEN
  SET $Discount = $Order/TotalAmount * 0.1;
END IF;

Date/Time Tokens

Mendix provides built-in date/time tokens enclosed in [% ... %]:

TokenDescription
[%CurrentDateTime%]Current date and time
[%BeginOfCurrentDay%]Start of today (00:00)
[%EndOfCurrentDay%]End of today (23:59:59)
[%BeginOfCurrentWeek%]Start of the current week
[%BeginOfCurrentMonth%]Start of the current month
[%BeginOfCurrentYear%]Start of the current year

Usage in expressions:

$Order = CREATE Sales.Order (
  OrderDate = [%CurrentDateTime%],
  DueDate = [%EndOfCurrentDay%]
);

RETRIEVE $TodayOrders FROM Sales.Order
  WHERE OrderDate > [%BeginOfCurrentDay%];

String Templates

String concatenation with + is the primary way to build dynamic strings:

SET $Message = 'Order ' + $Order/OrderNumber + ' has been ' + $Order/Status;
LOG INFO NODE 'Orders' 'Total: ' + toString($Total) + ' for customer ' + $Customer/Name;

Type Conversion

Use built-in functions for type conversion in expressions:

toString($IntValue)           -- Integer/Decimal/Boolean to String

Enumeration Values

Reference enumeration values with their qualified name:

$Order = CREATE Sales.Order (
  Status = Sales.OrderStatus.Draft
);

IF $Order/Status = Sales.OrderStatus.Confirmed THEN
  -- process confirmed order
END IF;

Alternatively, enumeration values can be referenced as plain strings when the context is unambiguous:

$Order = CREATE Sales.Order (
  Status = 'Draft'
);

Expression Contexts

Expressions appear in several places within microflow activities:

In Conditions (IF, WHILE, WHERE)

IF $Order/TotalAmount > 0 AND $Order/Status != 'Cancelled' THEN ...
WHILE $Counter < $MaxRetries BEGIN ... END WHILE;
RETRIEVE $Active FROM Sales.Order WHERE Status = 'Active';

In Attribute Assignments (CREATE, CHANGE)

$Order = CREATE Sales.Order (
  OrderDate = [%CurrentDateTime%],
  Description = 'Order for ' + $Customer/Name,
  TotalAmount = $Subtotal + $Tax
);

In SET Assignments

SET $Counter = $Counter + 1;
SET $IsEligible = $Customer/Age >= 18 AND $Customer/IsActive;
SET $FullName = $Customer/FirstName + ' ' + $Customer/LastName;

In LOG Messages

LOG INFO 'Processed ' + toString($Count) + ' orders';
LOG ERROR NODE 'Validation' 'Invalid amount: ' + toString($Order/TotalAmount);

In VALIDATION FEEDBACK Messages

VALIDATION FEEDBACK $Customer/Email MESSAGE 'Please provide a valid email address';

Nanoflows vs Microflows

Nanoflows are client-side logic flows that execute in the user’s browser or on mobile devices. They share the same MDL syntax as microflows but have a different set of capabilities and restrictions.

Key Differences

AspectMicroflowNanoflow
ExecutionServer-side (Mendix runtime)Client-side (browser/mobile)
Database accessFull (retrieve, commit, delete)No direct database access
TransactionsSupported (with rollback)Not supported
Java actionsSupportedNot supported
JavaScript actionsNot supportedSupported
Show pageSupportedSupported
Close pageSupportedSupported
NetworkRequires server round-tripNo network call (fast)
OfflineNot available offlineAvailable offline
Error handlingON ERROR blocksLimited error handling

When to Use Which

Use a microflow when you need to:

  • Retrieve data from or commit data to the database
  • Call external web services or REST APIs
  • Execute Java actions
  • Perform batch operations on large data sets
  • Use transactions with rollback support

Use a nanoflow when you need to:

  • Respond quickly to user actions without server delay
  • Perform client-side validation
  • Toggle UI state (show/hide elements)
  • Navigate between pages
  • Work offline on mobile devices

CREATE NANOFLOW Syntax

CREATE [OR REPLACE] NANOFLOW <Module.Name>
  [FOLDER '<path>']
BEGIN
  [<declarations>]
  [<activities>]
  [RETURN <value>;]
END;

The syntax is identical to CREATE MICROFLOW except for the keyword.

Supported Activities in Nanoflows

Object Operations

-- Create an object (in memory only)
$Item = CREATE Sales.CartItem (
  Quantity = 1,
  ProductName = $Product/Name
);

-- Change an object in memory
CHANGE $Item (Quantity = $Item/Quantity + 1);

Calling Other Flows

-- Call another nanoflow
$Result = CALL NANOFLOW Sales.NAV_ValidateCart (Cart = $Cart);

-- Call a microflow (triggers server round-trip)
$ServerResult = CALL MICROFLOW Sales.ACT_SubmitOrder (Order = $Order);

UI Activities

-- Show a page
SHOW PAGE Sales.CartDetail ($Cart = $Cart);

-- Close the current page
CLOSE PAGE;

Validation

VALIDATION FEEDBACK $Item/Quantity MESSAGE 'Quantity must be at least 1';

Logging

LOG INFO 'Cart updated with ' + toString($ItemCount) + ' items';

Control Flow

IF $Cart/ItemCount = 0 THEN
  VALIDATION FEEDBACK $Cart/ItemCount MESSAGE 'Cart is empty';
  RETURN false;
ELSE
  SHOW PAGE Sales.Checkout ($Cart = $Cart);
  RETURN true;
END IF;

Activities NOT Available in Nanoflows

The following activities are server-only and cannot be used in nanoflows:

  • RETRIEVE ... FROM Module.Entity WHERE ... (database retrieval)
  • COMMIT
  • DELETE
  • ROLLBACK
  • CALL JAVA ACTION
  • EXECUTE DATABASE QUERY
  • ON ERROR { ... } (full error handler blocks)

SHOW and DESCRIBE

SHOW NANOFLOWS
SHOW NANOFLOWS IN MyModule
DESCRIBE NANOFLOW MyModule.NAV_ShowDetails

DROP

DROP NANOFLOW MyModule.NAV_ShowDetails;

Folder Organization

CREATE NANOFLOW Sales.NAV_OpenCart
FOLDER 'Navigation'
BEGIN
  SHOW PAGE Sales.Cart_Overview ();
END;
MOVE NANOFLOW Sales.NAV_OpenCart TO FOLDER 'UI/Navigation';

Example: Client-Side Validation

CREATE NANOFLOW Sales.NAV_ValidateOrder
FOLDER 'Validation'
BEGIN
  DECLARE $Order Sales.Order;
  DECLARE $IsValid Boolean = true;

  IF $Order/CustomerName = empty THEN
    VALIDATION FEEDBACK $Order/CustomerName MESSAGE 'Customer name is required';
    SET $IsValid = false;
  END IF;

  IF $Order/TotalAmount <= 0 THEN
    VALIDATION FEEDBACK $Order/TotalAmount MESSAGE 'Total must be greater than zero';
    SET $IsValid = false;
  END IF;

  RETURN $IsValid;
END;

Example: Page Navigation

CREATE NANOFLOW Sales.NAV_GoToOrderDetail
BEGIN
  DECLARE $Order Sales.Order;

  SHOW PAGE Sales.Order_Detail ($Order = $Order);
END;

Common Patterns

This page shows frequently used microflow patterns in MDL, including CRUD operations, validation, batch processing, and important anti-patterns to avoid.

CRUD Patterns

Create and Commit

CREATE MICROFLOW Sales.ACT_CreateCustomer
FOLDER 'Customer'
BEGIN
  DECLARE $Name String;
  DECLARE $Email String;
  DECLARE $Customer Sales.Customer;

  $Customer = CREATE Sales.Customer (
    Name = $Name,
    Email = $Email,
    CreatedDate = [%CurrentDateTime%],
    IsActive = true
  );
  COMMIT $Customer;

  SHOW PAGE Sales.Customer_Edit ($Customer = $Customer);
  RETURN $Customer;
END;

Retrieve and Update

CREATE MICROFLOW Sales.ACT_DeactivateCustomer
FOLDER 'Customer'
BEGIN
  DECLARE $Customer Sales.Customer;

  CHANGE $Customer (
    IsActive = false,
    DeactivatedDate = [%CurrentDateTime%]
  );
  COMMIT $Customer;
END;

Retrieve with Filtering

CREATE MICROFLOW Sales.ACT_GetActiveOrders
FOLDER 'Orders'
BEGIN
  RETRIEVE $Orders FROM Sales.Order
    WHERE Status = 'Active'
    AND OrderDate > [%BeginOfCurrentMonth%];

  RETURN $Orders;
END;

Delete with Confirmation

CREATE MICROFLOW Sales.ACT_DeleteOrder
FOLDER 'Orders'
BEGIN
  DECLARE $Order Sales.Order;

  -- Delete related order lines first
  RETRIEVE $Lines FROM $Order/Sales.OrderLine_Order;
  LOOP $Line IN $Lines
  BEGIN
    DELETE $Line;
  END LOOP;

  DELETE $Order;
END;

Validation Pattern

Validate an object before saving, showing feedback on invalid fields:

CREATE MICROFLOW Sales.ACT_SaveCustomer
FOLDER 'Customer'
BEGIN
  DECLARE $Customer Sales.Customer;
  DECLARE $IsValid Boolean = true;

  -- Validate required fields
  IF $Customer/Name = empty THEN
    VALIDATION FEEDBACK $Customer/Name MESSAGE 'Name is required';
    SET $IsValid = false;
  END IF;

  IF $Customer/Email = empty THEN
    VALIDATION FEEDBACK $Customer/Email MESSAGE 'Email is required';
    SET $IsValid = false;
  END IF;

  -- Check for duplicate email
  IF $IsValid THEN
    RETRIEVE $Existing FROM Sales.Customer
      WHERE Email = $Customer/Email
      LIMIT 1;
    IF $Existing != empty THEN
      VALIDATION FEEDBACK $Customer/Email MESSAGE 'A customer with this email already exists';
      SET $IsValid = false;
    END IF;
  END IF;

  -- Save only if valid
  IF $IsValid THEN
    COMMIT $Customer;
    CLOSE PAGE;
  END IF;
END;

Batch Processing Pattern

Process a list of items one at a time with error tracking:

CREATE MICROFLOW Sales.ACT_ProcessPendingOrders
FOLDER 'Batch'
BEGIN
  DECLARE $SuccessCount Integer = 0;
  DECLARE $ErrorCount Integer = 0;

  RETRIEVE $PendingOrders FROM Sales.Order
    WHERE Status = 'Pending';

  LOOP $Order IN $PendingOrders
  BEGIN
    CALL MICROFLOW Sales.SUB_ProcessOrder (
      Order = $Order
    ) ON ERROR {
      LOG ERROR NODE 'BatchProcess' 'Failed to process order: ' + $Order/OrderNumber;
      SET $ErrorCount = $ErrorCount + 1;
      CONTINUE;
    };

    SET $SuccessCount = $SuccessCount + 1;
  END LOOP;

  LOG INFO NODE 'BatchProcess' 'Batch complete: '
    + toString($SuccessCount) + ' succeeded, '
    + toString($ErrorCount) + ' failed';
END;

Sub-Microflow Pattern

Break complex logic into reusable sub-microflows:

-- Main microflow delegates to sub-microflow
CREATE MICROFLOW Sales.ACT_PlaceOrder
FOLDER 'Orders'
BEGIN
  DECLARE $Order Sales.Order;

  -- Validate
  $IsValid = CALL MICROFLOW Sales.SUB_ValidateOrder (Order = $Order);
  IF NOT $IsValid THEN
    RETURN false;
  END IF;

  -- Calculate totals
  CALL MICROFLOW Sales.SUB_CalculateOrderTotals (Order = $Order);

  -- Finalize
  CHANGE $Order (Status = 'Placed');
  COMMIT $Order WITH EVENTS;

  RETURN true;
END;
-- Reusable sub-microflow
CREATE MICROFLOW Sales.SUB_CalculateOrderTotals
FOLDER 'Orders'
BEGIN
  DECLARE $Order Sales.Order;
  DECLARE $Total Decimal = 0;

  RETRIEVE $Lines FROM $Order/Sales.OrderLine_Order;
  LOOP $Line IN $Lines
  BEGIN
    SET $Total = $Total + ($Line/Quantity * $Line/UnitPrice);
  END LOOP;

  CHANGE $Order (TotalAmount = $Total);
END;

Association Retrieval Pattern

Retrieve related objects through associations:

CREATE MICROFLOW Sales.ACT_GetOrderSummary
FOLDER 'Orders'
BEGIN
  DECLARE $Order Sales.Order;

  -- Retrieve related customer
  RETRIEVE $Customer FROM $Order/Sales.Order_Customer;

  -- Retrieve order lines
  RETRIEVE $Lines FROM $Order/Sales.OrderLine_Order;

  -- Use the retrieved data
  LOG INFO 'Order ' + $Order/OrderNumber
    + ' for customer ' + $Customer/Name
    + ' has ' + toString(length($Lines)) + ' lines';

  RETURN $Order;
END;

When you have a list and need to find a matching item, retrieve from the list variable:

CREATE MICROFLOW Import.ACT_MatchDepartments
FOLDER 'Import'
BEGIN
  DECLARE $Employees List of Import.Employee = empty;
  DECLARE $Departments List of Import.Department = empty;

  -- Retrieve all departments for lookup
  RETRIEVE $Departments FROM Import.Department;

  LOOP $Employee IN $Employees
  BEGIN
    -- O(N) lookup: retrieve from in-memory list
    RETRIEVE $Dept FROM $Departments
      WHERE Name = $Employee/DepartmentName
      LIMIT 1;

    IF $Dept != empty THEN
      CHANGE $Employee (Employee_Department = $Dept);
      COMMIT $Employee;
    END IF;
  END LOOP;
END;

Error Handling Pattern

Wrap risky operations with error handlers:

CREATE MICROFLOW Integration.ACT_SyncData
FOLDER 'Integration'
BEGIN
  DECLARE $Config Integration.SyncConfig;
  DECLARE $Success Boolean = false;

  RETRIEVE $Config FROM Integration.SyncConfig LIMIT 1;

  @caption 'Call external API'
  $Response = CALL MICROFLOW Integration.SUB_CallExternalAPI (
    Config = $Config
  ) ON ERROR {
    LOG ERROR NODE 'Integration' 'External API call failed';
    RETURN false;
  };

  IF $Response != empty THEN
    @caption 'Process response'
    CALL MICROFLOW Integration.SUB_ProcessResponse (
      Response = $Response
    ) ON ERROR {
      LOG ERROR NODE 'Integration' 'Failed to process response';
      RETURN false;
    };
    SET $Success = true;
  END IF;

  RETURN $Success;
END;

Anti-Patterns (Avoid These)

The following patterns are common mistakes that cause bugs, performance problems, or parse errors. The mxcli check command detects these automatically.

Never Create Empty List Variables as Loop Sources

Creating an empty list and then immediately looping over it is always wrong. If you need to process a list, accept it as a microflow parameter or retrieve it from the database.

-- WRONG: empty list means the loop never executes
DECLARE $Items List of Sales.Item = empty;
LOOP $Item IN $Items
BEGIN
  -- this code never runs!
END LOOP;
-- CORRECT: accept the list as a parameter
CREATE MICROFLOW Sales.ACT_ProcessItems
BEGIN
  DECLARE $Items List of Sales.Item = empty;  -- parameter, filled by caller

  RETRIEVE $Items FROM Sales.Item WHERE Status = 'Pending';  -- or retrieve from DB

  LOOP $Item IN $Items
  BEGIN
    -- process each item
  END LOOP;
END;

Never Use Nested Loops for List Matching

Nested loops are O(N^2) and cause severe performance problems with large data sets. Instead, loop over the primary list and use RETRIEVE ... FROM $List WHERE ... LIMIT 1 to find matches.

-- WRONG: O(N^2) nested loop
LOOP $Employee IN $Employees
BEGIN
  LOOP $Department IN $Departments
  BEGIN
    IF $Department/Name = $Employee/DeptName THEN
      CHANGE $Employee (Employee_Department = $Department);
    END IF;
  END LOOP;
END LOOP;
-- CORRECT: O(N) lookup from list
LOOP $Employee IN $Employees
BEGIN
  RETRIEVE $Dept FROM $Departments
    WHERE Name = $Employee/DeptName
    LIMIT 1;

  IF $Dept != empty THEN
    CHANGE $Employee (Employee_Department = $Dept);
  END IF;
END LOOP;

Use Append Logic When Merging, Not Overwrite

When merging data from multiple sources, append new values rather than overwriting existing ones:

-- WRONG: overwrites existing notes
CHANGE $Customer (Notes = $NewNotes);
-- CORRECT: append with guard
IF $NewNotes != empty THEN
  CHANGE $Customer (Notes = $Customer/Notes + '\n' + $NewNotes);
END IF;

Naming Conventions

Follow consistent naming patterns for microflows:

PrefixPurposeExample
ACT_User-triggered actionSales.ACT_CreateOrder
SUB_Sub-microflow (called by other microflows)Sales.SUB_CalculateTotal
DS_Data source for a page/widgetSales.DS_GetActiveOrders
VAL_Validation logicSales.VAL_ValidateOrder
SE_Scheduled event handlerSales.SE_ProcessBatch
BCO_Before commit eventSales.BCO_Order
ACO_After commit eventSales.ACO_Order

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 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

ConceptDescription
LayoutA reusable page template that defines content regions (e.g., header, sidebar, main content)
Widget treeA hierarchical structure of widgets that defines the page’s visual content
Data sourceDetermines how a widget obtains its data (page parameter, database query, microflow, etc.)
Widget nameEvery widget has a unique name within the page, used for ALTER PAGE operations

CREATE PAGE

The basic syntax for creating a page:

CREATE [OR REPLACE] PAGE <Module>.<Name>
(
  [Params: { $Param: Module.Entity [, ...] },]
  Title: '<title>',
  Layout: <Module.LayoutName>
  [, Folder: '<path>']
)
{
  <widget-tree>
}

Minimal Example

CREATE PAGE MyModule.Home
(
  Title: 'Welcome',
  Layout: Atlas_Core.Atlas_Default
)
{
  CONTAINER cMain {
    DYNAMICTEXT txtWelcome (Content: 'Welcome to the application')
  }
}

Page with Parameters

Pages can receive entity objects as parameters from the calling context:

CREATE PAGE MyModule.Customer_Edit
(
  Params: { $Customer: MyModule.Customer },
  Title: 'Edit Customer',
  Layout: Atlas_Core.PopupLayout
)
{
  DATAVIEW dvCustomer (DataSource: $Customer) {
    TEXTBOX txtName (Label: 'Name', Attribute: Name)
    TEXTBOX txtEmail (Label: 'Email', Attribute: Email)
    FOOTER footer1 {
      ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)
      ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES)
    }
  }
}

Page Properties

PropertyDescriptionExample
ParamsPage parameters (entity objects passed in)Params: { $Order: Sales.Order }
TitlePage title shown in the browser/tabTitle: 'Edit Customer'
LayoutLayout to use for the pageLayout: Atlas_Core.PopupLayout
FolderOrganizational folder within the moduleFolder: 'Pages/Customers'
VariablesPage-level variables for conditional logicVariables: { $show: Boolean = 'true' }

Layouts

Layouts are referenced by their qualified name (Module.LayoutName). Common Atlas layouts include:

LayoutUsage
Atlas_Core.Atlas_DefaultFull-page layout with navigation sidebar
Atlas_Core.PopupLayoutModal popup dialog
Atlas_Core.Atlas_TopBarLayout with top navigation bar

DROP PAGE

Removes a page from the project:

DROP PAGE MyModule.Customer_Edit;

Inspecting Pages

Use SHOW and DESCRIBE to examine existing pages:

-- List all pages in a module
SHOW PAGES IN MyModule;

-- Show the full MDL definition of a page (round-trippable)
DESCRIBE PAGE MyModule.Customer_Edit;

The output of DESCRIBE PAGE can be used as input to CREATE OR REPLACE PAGE for round-trip editing.

See Also

Page Structure

Every MDL page has three main parts: page properties (title, layout, parameters), a layout reference that defines the page skeleton, and a widget tree that fills the content area.

CREATE PAGE Syntax

CREATE [OR REPLACE] PAGE <Module>.<Name>
(
  [Params: { $Param: Module.Entity [, ...] },]
  Title: '<title>',
  Layout: <Module.LayoutName>
  [, Folder: '<path>']
  [, Variables: { $name: Type = 'expression' [, ...] }]
)
{
  <widget-tree>
}

Layout Reference

The Layout property selects the page layout, which determines the overall structure (navigation sidebar, top bar, popup frame, etc.). The widget tree you define fills the layout’s content placeholder.

CREATE PAGE MyModule.CustomerList
(
  Title: 'Customers',
  Layout: Atlas_Core.Atlas_Default
)
{
  -- Widgets here fill the main content area of Atlas_Default
  DATAGRID dgCustomers (DataSource: DATABASE MyModule.Customer) {
    COLUMN colName (Attribute: Name, Caption: 'Name')
    COLUMN colEmail (Attribute: Email, Caption: 'Email')
  }
}

Popup layouts are typically used for edit and detail pages:

CREATE PAGE MyModule.Customer_Edit
(
  Params: { $Customer: MyModule.Customer },
  Title: 'Edit Customer',
  Layout: Atlas_Core.PopupLayout
)
{
  DATAVIEW dvCustomer (DataSource: $Customer) {
    TEXTBOX txtName (Label: 'Name', Attribute: Name)
    FOOTER footer1 {
      ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)
      ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES)
    }
  }
}

Page Parameters

Page parameters define entity objects that must be passed when the page is opened. Parameters use the $ prefix:

(
  Params: { $Customer: MyModule.Customer },
  ...
)

Multiple parameters are comma-separated:

(
  Params: { $Order: Sales.Order, $Customer: Sales.Customer },
  ...
)

Inside the widget tree, a DATAVIEW binds to a parameter using DataSource: $ParamName.

Page Variables

Page variables store local state (booleans, strings, etc.) that can control widget visibility or other conditional logic:

(
  Title: 'Product Detail',
  Layout: Atlas_Core.Atlas_Default,
  Variables: { $showDetails: Boolean = 'true' }
)

Data Sources

Data sources tell a container widget (DataView, DataGrid, ListView, Gallery) where to get its data.

Page Parameter Source

Binds to a page parameter. Used by DataView widgets for edit/detail pages:

DATAVIEW dvCustomer (DataSource: $Customer) {
  -- widgets bound to Customer attributes
}

Database Source

Retrieves entities directly from the database. Optionally includes an XPath constraint:

DATAGRID dgOrders (DataSource: DATABASE Sales.Order) {
  COLUMN colId (Attribute: OrderId, Caption: 'Order #')
  COLUMN colDate (Attribute: OrderDate, Caption: 'Date')
}

Microflow Source

Calls a microflow that returns a list or single object:

DATAVIEW dvDashboard (DataSource: MICROFLOW MyModule.DS_GetDashboardData) {
  -- widgets bound to the returned object's attributes
}

Nanoflow Source

Same as microflow source but calls a nanoflow (runs on the client):

LISTVIEW lvRecent (DataSource: NANOFLOW MyModule.DS_GetRecentItems) {
  -- widgets for each item
}

Association Source

Follows an association from a parent DataView’s object:

DATAVIEW dvCustomer (DataSource: $Customer) {
  LISTVIEW lvOrders (DataSource: ASSOCIATION Customer_Order) {
    -- widgets for each Order
  }
}

Selection Source

Binds to the currently selected item in another list widget:

DATAGRID dgProducts (DataSource: DATABASE MyModule.Product) {
  COLUMN colName (Attribute: Name)
}

DATAVIEW dvDetail (DataSource: SELECTION dgProducts) {
  TEXTBOX txtDescription (Label: 'Description', Attribute: Description)
}

Widget Tree Structure

The widget tree is a nested hierarchy. Container widgets hold child widgets within { } braces. Every widget requires a unique name:

{
  LAYOUTGRID grid1 {
    ROW row1 {
      COLUMN col1 {
        TEXTBOX txtName (Label: 'Name', Attribute: Name)
      }
      COLUMN col2 {
        TEXTBOX txtEmail (Label: 'Email', Attribute: Email)
      }
    }
  }
}

Folder Organization

Use the Folder property to organize pages into folders within a module:

CREATE PAGE MyModule.Customer_Edit
(
  Params: { $Customer: MyModule.Customer },
  Title: 'Edit Customer',
  Layout: Atlas_Core.PopupLayout,
  Folder: 'Customers'
)
{
  ...
}

Nested folders use / separators: Folder: 'Pages/Customers/Detail'. Missing folders are auto-created.

See Also

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

CategoryWidgets
LayoutLAYOUTGRID, ROW, COLUMN, CONTAINER, CUSTOMCONTAINER
DataDATAVIEW, LISTVIEW, DATAGRID, GALLERY
InputTEXTBOX, TEXTAREA, CHECKBOX, RADIOBUTTONS, DATEPICKER, COMBOBOX
DisplayDYNAMICTEXT, IMAGE, STATICIMAGE, DYNAMICIMAGE
ActionACTIONBUTTON, LINKBUTTON
NavigationNAVIGATIONLIST
StructureHEADER, FOOTER, CONTROLBAR, SNIPPETCALL

Layout Widgets

LAYOUTGRID

Creates a responsive grid with rows and columns. The primary layout mechanism for arranging widgets on a page:

LAYOUTGRID grid1 {
  ROW row1 {
    COLUMN col1 {
      TEXTBOX txtName (Label: 'Name', Attribute: Name)
    }
    COLUMN col2 {
      TEXTBOX txtEmail (Label: 'Email', Attribute: Email)
    }
  }
  ROW row2 {
    COLUMN colFull {
      TEXTAREA txtNotes (Label: 'Notes', Attribute: Notes)
    }
  }
}

ROW

A row within a LAYOUTGRID. Contains one or more COLUMN children.

COLUMN

A column within a ROW. Contains any number of child widgets. Column width is determined by the layout grid system.

CONTAINER

A generic div container. Used to group widgets and apply CSS classes or styles:

CONTAINER cCard (Class: 'card mx-spacing-top-large') {
  DYNAMICTEXT txtTitle (Content: 'Section Title')
  TEXTBOX txtValue (Label: 'Value', Attribute: Value)
}

Properties:

PropertyDescriptionExample
ClassCSS class namesClass: 'card p-3'
StyleInline CSS stylesStyle: 'padding: 16px;'
DesignPropertiesDesign property valuesDesignProperties: ['Spacing top': 'Large']

CUSTOMCONTAINER

Similar to CONTAINER but used for custom-styled containers.

Data Widgets

DATAVIEW

Displays a single object. The central widget for detail and edit pages. Must have a data source:

DATAVIEW dvCustomer (DataSource: $Customer) {
  TEXTBOX txtName (Label: 'Name', Attribute: Name)
  TEXTBOX txtEmail (Label: 'Email', Attribute: Email)
  COMBOBOX cbStatus (Label: 'Status', Attribute: Status)
  FOOTER footer1 {
    ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)
    ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES)
  }
}

Data source options:

  • DataSource: $ParamName – page 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

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:

PropertyDescriptionExample
DataSourceData source (DATABASE, MICROFLOW, etc.)DataSource: DATABASE Module.Entity
PageSizeNumber of rows per pagePageSize: 25
PaginationPagination modePagination: virtualScrolling
PagingPositionPosition of paging controlsPagingPosition: both
ShowPagingButtonsWhen to show paging buttonsShowPagingButtons: auto

Column properties:

PropertyValuesDefaultDescription
Attributeattribute name(required)The attribute to display
Captionstringattribute nameColumn header text
Alignmentleft, center, rightleftText alignment
WrapTexttrue, falsefalseAllow text wrapping
Sortabletrue, falsevariesAllow sorting by this column
Resizabletrue, falsetrueAllow column resizing
Draggabletrue, falsetrueAllow column reordering
Hidableyes, hidden, noyesUser visibility toggle
ColumnWidthautoFill, autoFit, manualautoFillWidth mode
Sizeinteger (px)1Width in pixels (manual mode)
Visibleexpression stringtrueVisibility expression
DynamicCellClassexpression string(empty)Dynamic CSS class expression
Tooltiptext string(empty)Column tooltip

LISTVIEW

Displays a list of objects using a repeating template. More flexible than DataGrid for custom layouts:

LISTVIEW lvProducts (DataSource: DATABASE MyModule.Product) {
  CONTAINER cItem (Class: 'list-item') {
    DYNAMICTEXT txtName (Content: '{1}', Attribute: Name)
    DYNAMICTEXT txtPrice (Content: '${1}', Attribute: Price)
  }
}

A pluggable widget that displays items in a card/grid layout:

GALLERY galProducts (DataSource: DATABASE MyModule.Product) {
  CONTAINER cCard {
    DYNAMICTEXT txtName (Content: '{1}', Attribute: Name)
  }
}

Input Widgets

All input widgets share common properties:

PropertyDescriptionExample
LabelField label textLabel: 'Customer Name'
AttributeEntity attribute to bind toAttribute: Name
EditableEditability modeEditable: ReadOnly
VisibleVisibility expressionVisible: '$showField'

TEXTBOX

Single-line text input. The most common input widget:

TEXTBOX txtName (Label: 'Name', Attribute: Name)
TEXTBOX txtEmail (Label: 'Email', Attribute: Email)

TEXTAREA

Multi-line text input for longer text:

TEXTAREA txtDescription (Label: 'Description', Attribute: Description)

CHECKBOX

Boolean (true/false) input:

CHECKBOX cbActive (Label: 'Active', Attribute: IsActive)

RADIOBUTTONS

Displays enumeration or boolean values as radio buttons:

RADIOBUTTONS rbStatus (Label: 'Status', Attribute: Status)

DATEPICKER

Date and/or time input:

DATEPICKER dpBirthDate (Label: 'Birth Date', Attribute: BirthDate)

COMBOBOX

Dropdown selection for enumeration values or associations. Uses the pluggable ComboBox widget:

COMBOBOX cbStatus (Label: 'Status', Attribute: Status)

REFERENCESELECTOR

Dropdown for selecting an associated object via a reference association:

REFERENCESELECTOR rsCategory (Label: 'Category', Attribute: Category)

Display Widgets

DYNAMICTEXT

Displays dynamic text content, often with attribute values:

DYNAMICTEXT txtGreeting (Content: 'Welcome, {1}', Attribute: Name)

IMAGE / STATICIMAGE / DYNAMICIMAGE

Display images on a page:

-- Static image from the project
STATICIMAGE imgLogo (Image: 'MyModule.Logo')

-- Dynamic image from an entity attribute (entity must extend System.Image)
DYNAMICIMAGE imgPhoto (DataSource: $Photo, Width: 200, Height: 150)

-- Generic image widget
IMAGE imgBanner (Width: 800, Height: 200)

Image properties:

PropertyDescriptionExample
WidthWidth in pixelsWidth: 200
HeightHeight in pixelsHeight: 150

Action Widgets

ACTIONBUTTON

A button that triggers an action. The primary interactive element:

ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)
ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES)
ACTIONBUTTON btnDelete (Caption: 'Delete', Action: DELETE, ButtonStyle: Danger)

Action types:

ActionDescription
SAVE_CHANGESCommit and close the page
CANCEL_CHANGESRoll back and close the page
DELETEDelete the current object
CLOSE_PAGEClose the page without saving
MICROFLOW Module.MF_NameCall a microflow
NANOFLOW Module.NF_NameCall a nanoflow
PAGE Module.PageNameOpen a page

Button styles:

StyleTypical appearance
DefaultStandard button
PrimaryBlue/highlighted button
SuccessGreen button
WarningYellow/amber button
DangerRed button
InfoLight blue button

Microflow action with parameters:

ACTIONBUTTON btnProcess (
  Caption: 'Process',
  Action: MICROFLOW Sales.ACT_ProcessOrder(Order: $Order),
  ButtonStyle: Primary
)

LINKBUTTON

Renders as a hyperlink instead of a button. Same action types as ACTIONBUTTON:

LINKBUTTON lnkDetails (Caption: 'View Details', Action: PAGE MyModule.Customer_Detail)

Structure Widgets

Header 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 section of a DataView. Typically contains save/cancel buttons:

FOOTER footer1 {
  ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)
  ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES)
}

CONTROLBAR

Control bar for DataGrid widgets. Contains action buttons for the grid:

CONTROLBAR bar1 {
  ACTIONBUTTON btnNew (Caption: 'New', Action: MICROFLOW Module.ACT_Create, ButtonStyle: Primary)
  ACTIONBUTTON btnEdit (Caption: 'Edit', Action: PAGE Module.Entity_Edit)
  ACTIONBUTTON btnDelete (Caption: 'Delete', Action: DELETE, ButtonStyle: Danger)
}

SNIPPETCALL

Embeds a snippet (reusable page fragment) into the page:

SNIPPETCALL scNav (Snippet: MyModule.NavigationMenu)

Renders a navigation list with clickable items:

NAVIGATIONLIST navMain {
  -- navigation items
}

Common Widget Properties

These properties are shared across many widget types:

PropertyDescriptionExample
ClassCSS class namesClass: 'card p-3'
StyleInline CSS stylesStyle: 'margin-top: 8px;'
DesignPropertiesAtlas design propertiesDesignProperties: ['Spacing top': 'Large', 'Full width': ON]
VisibleVisibility expressionVisible: '$showSection'
EditableEditability modeEditable: ReadOnly

See Also

Data Binding

Data binding connects widgets to entity attributes and data sources. In MDL, binding is configured through widget properties rather than a special operator.

Attribute Binding

Input widgets bind to entity attributes using the Attribute property. The attribute name is resolved relative to the containing DataView’s entity context:

DATAVIEW dvCustomer (DataSource: $Customer) {
  -- These attribute names are resolved against MyModule.Customer
  TEXTBOX txtName (Label: 'Name', Attribute: Name)
  TEXTBOX txtEmail (Label: 'Email', Attribute: Email)
  CHECKBOX cbActive (Label: 'Active', Attribute: IsActive)
  DATEPICKER dpCreated (Label: 'Created', Attribute: CreatedDate)
  COMBOBOX cbStatus (Label: 'Status', Attribute: Status)
}

The SDK automatically resolves short attribute names to fully qualified paths. For example, Name is resolved to MyModule.Customer.Name based on the DataView’s entity context.

Data Sources

Data sources determine where a container widget gets its data. Different widget types support different data source types.

Page Parameter Source

The simplest binding – connects a DataView to a page parameter:

CREATE PAGE MyModule.Customer_Edit
(
  Params: { $Customer: MyModule.Customer },
  Title: 'Edit Customer',
  Layout: Atlas_Core.PopupLayout
)
{
  DATAVIEW dvCustomer (DataSource: $Customer) {
    TEXTBOX txtName (Label: 'Name', Attribute: Name)
  }
}

Database Source

Retrieves entities directly from the database. Works with list widgets (DataGrid, ListView, Gallery):

DATAGRID dgCustomers (DataSource: DATABASE MyModule.Customer) {
  COLUMN colName (Attribute: Name, Caption: 'Customer Name')
  COLUMN colEmail (Attribute: Email, Caption: 'Email')
}

Microflow Source

Calls a microflow to retrieve data. The microflow must return the appropriate type (single object for DataView, list for DataGrid/ListView):

-- Single object for a DataView
DATAVIEW dvStats (DataSource: MICROFLOW MyModule.DS_GetStatistics) {
  DYNAMICTEXT txtCount (Content: 'Total: {1}', Attribute: TotalCount)
}

-- List for a DataGrid
DATAGRID dgFiltered (DataSource: MICROFLOW MyModule.DS_GetFilteredOrders) {
  COLUMN colId (Attribute: OrderId)
}

Nanoflow Source

Same as microflow source but runs on the client side:

LISTVIEW lvRecent (DataSource: NANOFLOW MyModule.DS_GetRecentItems) {
  DYNAMICTEXT txtItem (Content: '{1}', Attribute: Name)
}

Association Source

Follows an association from a parent DataView to retrieve related objects:

DATAVIEW dvCustomer (DataSource: $Customer) {
  TEXTBOX txtName (Label: 'Name', Attribute: Name)

  -- Follow Customer_Order association to list orders
  LISTVIEW lvOrders (DataSource: ASSOCIATION Customer_Order) {
    DYNAMICTEXT txtOrderDate (Content: '{1}', Attribute: OrderDate)
    DYNAMICTEXT txtAmount (Content: '${1}', Attribute: Amount)
  }
}

Selection Source

Binds to the currently selected item in another list widget. Enables master-detail patterns:

-- Master: list of products
DATAGRID dgProducts (DataSource: DATABASE MyModule.Product) {
  COLUMN colName (Attribute: Name)
  COLUMN colPrice (Attribute: Price)
}

-- Detail: shows the selected product
DATAVIEW dvDetail (DataSource: SELECTION dgProducts) {
  TEXTBOX txtDescription (Label: 'Description', Attribute: Description)
  TEXTBOX txtCategory (Label: 'Category', Attribute: Category)
}

Nested Data Contexts

Widgets inherit the data context from their parent container. A DataView inside a DataView creates a nested context:

DATAVIEW dvOrder (DataSource: $Order) {
  TEXTBOX txtOrderId (Label: 'Order #', Attribute: OrderId)

  -- Nested DataView for the order's customer (via association)
  DATAVIEW dvCustomer (DataSource: ASSOCIATION Order_Customer) {
    -- Attributes here resolve against Customer
    DYNAMICTEXT txtCustomerName (Content: '{1}', Attribute: Name)
  }
}

Dynamic Text Content

The DYNAMICTEXT widget uses {1}, {2}, etc. as placeholders for attribute values in the Content property. The Attribute property specifies which attribute fills the first placeholder:

DYNAMICTEXT txtPrice (Content: 'Price: ${1}', Attribute: Price)

Button Actions with Parameters

Action buttons can pass the current data context to microflows and pages:

DATAVIEW dvOrder (DataSource: $Order) {
  ACTIONBUTTON btnProcess (
    Caption: 'Process Order',
    Action: MICROFLOW Sales.ACT_ProcessOrder(Order: $Order),
    ButtonStyle: Primary
  )

  ACTIONBUTTON btnEdit (
    Caption: 'Edit',
    Action: PAGE Sales.Order_Edit
  )
}

See Also

Snippets

Snippets are reusable page fragments that can be embedded in multiple pages. They allow you to define a widget tree once and include it wherever needed, promoting consistency and reducing duplication.

CREATE SNIPPET

CREATE [OR REPLACE] SNIPPET <Module>.<Name>
(
  [Params: { $Param: Module.Entity [, ...] },]
  [Folder: '<path>']
)
{
  <widget-tree>
}

Basic Snippet

A snippet without parameters:

CREATE SNIPPET MyModule.Footer
(
  Folder: 'Snippets'
)
{
  CONTAINER cFooter (Class: 'app-footer') {
    DYNAMICTEXT txtCopyright (Content: '2024 My Company. All rights reserved.')
  }
}

Snippet with Parameters

Snippets can accept entity parameters, similar to pages:

CREATE SNIPPET MyModule.CustomerCard
(
  Params: { $Customer: MyModule.Customer }
)
{
  CONTAINER cCard (Class: 'card') {
    DATAVIEW dvCustomer (DataSource: $Customer) {
      DYNAMICTEXT txtName (Content: '{1}', Attribute: Name)
      DYNAMICTEXT txtEmail (Content: '{1}', Attribute: Email)
      ACTIONBUTTON btnEdit (
        Caption: 'Edit',
        Action: PAGE MyModule.Customer_Edit,
        ButtonStyle: Primary
      )
    }
  }
}

Using Snippets in Pages

Embed a snippet in a page using the SNIPPETCALL widget:

CREATE PAGE MyModule.Home
(
  Title: 'Home',
  Layout: Atlas_Core.Atlas_Default
)
{
  CONTAINER cMain {
    SNIPPETCALL scFooter (Snippet: MyModule.Footer)
  }
}

Inspecting Snippets

-- List all snippets in a module
SHOW SNIPPETS IN MyModule;

-- View full MDL definition
DESCRIBE SNIPPET MyModule.CustomerCard;

DROP SNIPPET

DROP SNIPPET MyModule.Footer;

ALTER SNIPPET

Snippets support the same in-place modification operations as pages. See ALTER PAGE / ALTER SNIPPET:

ALTER SNIPPET MyModule.CustomerCard {
  SET Caption = 'View Details' ON btnEdit;
  INSERT AFTER txtEmail {
    DYNAMICTEXT txtPhone (Content: '{1}', Attribute: Phone)
  }
};

See Also

ALTER PAGE / ALTER SNIPPET

The ALTER PAGE and ALTER SNIPPET statements modify an existing page or snippet’s widget tree in-place, without requiring a full CREATE OR REPLACE. This is especially useful for incremental changes: adding a field, changing a button caption, or removing an unused widget.

ALTER operates directly on the raw widget tree, preserving any widget types that MDL does not natively support (pluggable widgets, custom widgets, etc.).

Syntax

ALTER PAGE <Module>.<Name> {
  <operations>
};

ALTER SNIPPET <Module>.<Name> {
  <operations>
};

Operations

SET – Modify Widget Properties

Change one or more properties on a widget identified by name:

-- Single property
ALTER PAGE Module.EditPage {
  SET Caption = 'Save & Close' ON btnSave
};

-- Multiple properties at once
ALTER PAGE Module.EditPage {
  SET (Caption = 'Save & Close', ButtonStyle = Success) ON btnSave
};

Supported SET properties:

PropertyDescriptionExample
CaptionButton/link captionSET Caption = 'Submit' ON btnSave
LabelInput field labelSET Label = 'Full Name' ON txtName
ButtonStyleButton visual styleSET ButtonStyle = Danger ON btnDelete
ClassCSS class namesSET Class = 'card p-3' ON cMain
StyleInline CSSSET Style = 'margin: 8px;' ON cBox
EditableEditability modeSET Editable = ReadOnly ON txtEmail
VisibleVisibility expressionSET Visible = '$showField' ON txtPhone
NameWidget nameSET Name = 'txtFullName' ON txtName

SET – Page-Level Properties

Omit the ON clause to set page-level properties:

ALTER PAGE Module.EditPage {
  SET Title = 'Customer Details'
};

SET – Pluggable Widget Properties

Use quoted property names to set properties on pluggable widgets (ComboBox, DataGrid2, etc.):

ALTER PAGE Module.EditPage {
  SET 'showLabel' = false ON cbStatus
};

INSERT – Add Widgets

Insert new widgets before or after an existing widget:

-- Insert after a widget
ALTER PAGE Module.EditPage {
  INSERT AFTER txtEmail {
    TEXTBOX txtPhone (Label: 'Phone', Attribute: Phone)
    TEXTBOX txtFax (Label: 'Fax', Attribute: Fax)
  }
};

-- Insert before a widget
ALTER PAGE Module.EditPage {
  INSERT BEFORE btnSave {
    ACTIONBUTTON btnPreview (Caption: 'Preview', Action: MICROFLOW Module.ACT_Preview)
  }
};

The inserted widgets use the same syntax as in CREATE PAGE. Multiple widgets can be inserted in a single block.

DROP WIDGET – Remove Widgets

Remove one or more widgets by name:

ALTER PAGE Module.EditPage {
  DROP WIDGET txtUnused
};

-- Multiple widgets
ALTER PAGE Module.EditPage {
  DROP WIDGET txtFax, txtPager, btnObsolete
};

Dropping a container widget also removes all of its children.

REPLACE – Replace a Widget

Replace a widget (and its subtree) with new widgets:

ALTER PAGE Module.EditPage {
  REPLACE txtOldField WITH {
    TEXTAREA txtNotes (Label: 'Notes', Attribute: Notes)
  }
};

Page Variables

Add or remove page-level variables:

-- Add a variable
ALTER PAGE Module.EditPage {
  ADD Variables $showAdvanced: Boolean = 'false'
};

-- Remove a variable
ALTER PAGE Module.EditPage {
  DROP Variables $showAdvanced
};

Combining Operations

Multiple operations can be combined in a single ALTER statement. They are applied in order:

ALTER PAGE Module.Customer_Edit {
  -- Change button appearance
  SET (Caption = 'Save & Close', ButtonStyle = Success) ON btnSave;

  -- Remove unused fields
  DROP WIDGET txtFax;

  -- Add new fields after email
  INSERT AFTER txtEmail {
    TEXTBOX txtPhone (Label: 'Phone', Attribute: Phone)
    TEXTBOX txtMobile (Label: 'Mobile', Attribute: Mobile)
  };

  -- Replace old status dropdown with combobox
  REPLACE ddStatus WITH {
    COMBOBOX cbStatus (Label: 'Status', Attribute: Status)
  }
};

Workflow Tips

  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

Add a Field to an Edit Page

-- First, check what's on the page
DESCRIBE PAGE MyModule.Customer_Edit;

-- Add a phone field after email
ALTER PAGE MyModule.Customer_Edit {
  INSERT AFTER txtEmail {
    TEXTBOX txtPhone (Label: 'Phone Number', Attribute: Phone)
  }
};

Change Button Behavior

ALTER PAGE MyModule.Order_Edit {
  SET (Caption = 'Submit Order', ButtonStyle = Success) ON btnSave;
  SET Caption = 'Discard' ON btnCancel
};

Add a DataGrid Column

Since DataGrid columns are part of the widget tree, use INSERT to add columns:

ALTER PAGE MyModule.Customer_Overview {
  INSERT AFTER colEmail {
    COLUMN colPhone (Attribute: Phone, Caption: 'Phone')
  }
};

See Also

Common Patterns

This page collects frequently used page patterns: overview/list pages, edit pages, and master-detail layouts. Each pattern is a complete, working example.

Overview / List Page

An overview page displays a list of entities with a control bar for creating, editing, and deleting records. Clicking a row opens the edit page.

CREATE PAGE MyModule.Customer_Overview
(
  Title: 'Customers',
  Layout: Atlas_Core.Atlas_Default,
  Folder: 'Customers'
)
{
  DATAGRID dgCustomers (DataSource: DATABASE MyModule.Customer, PageSize: 20) {
    COLUMN colName (Attribute: Name, Caption: 'Name')
    COLUMN colEmail (Attribute: Email, Caption: 'Email')
    COLUMN colStatus (Attribute: Status, Caption: 'Status')
    COLUMN colCreated (Attribute: CreatedDate, Caption: 'Created')
    CONTROLBAR bar1 {
      ACTIONBUTTON btnNew (
        Caption: 'New Customer',
        Action: MICROFLOW MyModule.ACT_Customer_New,
        ButtonStyle: Primary
      )
      ACTIONBUTTON btnEdit (Caption: 'Edit', Action: PAGE MyModule.Customer_Edit)
      ACTIONBUTTON btnDelete (Caption: 'Delete', Action: DELETE, ButtonStyle: Danger)
    }
  }
}

The “New Customer” button calls a microflow that creates a new Customer object and opens the edit page:

CREATE MICROFLOW MyModule.ACT_Customer_New
BEGIN
  DECLARE $Customer MyModule.Customer;
  $Customer = CREATE MyModule.Customer (
    IsActive = true
  );
  SHOW PAGE MyModule.Customer_Edit ($Customer = $Customer);
  RETURN $Customer;
END;

Edit Page (Popup)

An edit page displayed as a popup dialog. Receives the entity as a page parameter:

CREATE PAGE MyModule.Customer_Edit
(
  Params: { $Customer: MyModule.Customer },
  Title: 'Edit Customer',
  Layout: Atlas_Core.PopupLayout,
  Folder: 'Customers'
)
{
  DATAVIEW dvCustomer (DataSource: $Customer) {
    TEXTBOX txtName (Label: 'Name', Attribute: Name)
    TEXTBOX txtEmail (Label: 'Email', Attribute: Email)
    TEXTBOX txtPhone (Label: 'Phone', Attribute: Phone)
    COMBOBOX cbStatus (Label: 'Status', Attribute: Status)
    CHECKBOX cbActive (Label: 'Active', Attribute: IsActive)
    FOOTER footer1 {
      ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)
      ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES)
    }
  }
}

Detail Page (Full Page)

A full-page detail view with sections organized using layout grids:

CREATE PAGE MyModule.Customer_Detail
(
  Params: { $Customer: MyModule.Customer },
  Title: 'Customer Detail',
  Layout: Atlas_Core.Atlas_Default,
  Folder: 'Customers'
)
{
  DATAVIEW dvCustomer (DataSource: $Customer) {
    LAYOUTGRID grid1 {
      ROW rowBasic {
        COLUMN col1 {
          TEXTBOX txtName (Label: 'Name', Attribute: Name)
          TEXTBOX txtEmail (Label: 'Email', Attribute: Email)
        }
        COLUMN col2 {
          TEXTBOX txtPhone (Label: 'Phone', Attribute: Phone)
          COMBOBOX cbStatus (Label: 'Status', Attribute: Status)
        }
      }
      ROW rowNotes {
        COLUMN colNotes {
          TEXTAREA txtNotes (Label: 'Notes', Attribute: Notes)
        }
      }
    }
    FOOTER footer1 {
      ACTIONBUTTON btnEdit (
        Caption: 'Edit',
        Action: PAGE MyModule.Customer_Edit,
        ButtonStyle: Primary
      )
      ACTIONBUTTON btnBack (Caption: 'Back', Action: CLOSE_PAGE)
    }
  }
}

Master-Detail Pattern

A master-detail page shows a list on one side and the selected item’s details on the other. The SELECTION data source connects the detail view to the list.

Side-by-Side Layout

CREATE PAGE MyModule.Product_MasterDetail
(
  Title: 'Products',
  Layout: Atlas_Core.Atlas_Default,
  Folder: 'Products'
)
{
  LAYOUTGRID gridMain {
    ROW rowContent {
      COLUMN colList {
        DATAGRID dgProducts (DataSource: DATABASE MyModule.Product, PageSize: 15) {
          COLUMN colName (Attribute: Name, Caption: 'Product')
          COLUMN colPrice (Attribute: Price, Caption: 'Price', Alignment: right)
          COLUMN colCategory (Attribute: Category, Caption: 'Category')
          CONTROLBAR bar1 {
            ACTIONBUTTON btnNew (
              Caption: 'New',
              Action: MICROFLOW MyModule.ACT_Product_New,
              ButtonStyle: Primary
            )
          }
        }
      }
      COLUMN colDetail {
        DATAVIEW dvDetail (DataSource: SELECTION dgProducts) {
          TEXTBOX txtName (Label: 'Name', Attribute: Name)
          TEXTBOX txtDescription (Label: 'Description', Attribute: Description)
          TEXTBOX txtPrice (Label: 'Price', Attribute: Price)
          COMBOBOX cbCategory (Label: 'Category', Attribute: Category)
          FOOTER footer1 {
            ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)
          }
        }
      }
    }
  }
}

A master-detail pattern where selecting an entity also shows its related child entities via an association:

CREATE PAGE MyModule.Order_MasterDetail
(
  Title: 'Orders',
  Layout: Atlas_Core.Atlas_Default,
  Folder: 'Orders'
)
{
  LAYOUTGRID gridMain {
    ROW rowContent {
      COLUMN colOrders {
        DATAGRID dgOrders (DataSource: DATABASE MyModule.Order, PageSize: 10) {
          COLUMN colOrderId (Attribute: OrderId, Caption: 'Order #')
          COLUMN colDate (Attribute: OrderDate, Caption: 'Date')
          COLUMN colStatus (Attribute: Status, Caption: 'Status')
          COLUMN colTotal (Attribute: TotalAmount, Caption: 'Total', Alignment: right)
        }
      }
      COLUMN colDetail {
        DATAVIEW dvOrder (DataSource: SELECTION dgOrders) {
          DYNAMICTEXT txtOrderInfo (Content: 'Order #{1}', Attribute: OrderId)
          TEXTBOX txtStatus (Label: 'Status', Attribute: Status)

          -- Order lines via association
          DATAGRID dgLines (DataSource: ASSOCIATION Order_OrderLine) {
            COLUMN colProduct (Attribute: ProductName, Caption: 'Product')
            COLUMN colQty (Attribute: Quantity, Caption: 'Qty', Alignment: right)
            COLUMN colLineTotal (Attribute: LineTotal, Caption: 'Total', Alignment: right)
          }

          FOOTER footer1 {
            ACTIONBUTTON btnEdit (
              Caption: 'Edit Order',
              Action: PAGE MyModule.Order_Edit,
              ButtonStyle: Primary
            )
          }
        }
      }
    }
  }
}

CRUD Page Set

A complete set of pages for managing an entity typically includes:

  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

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

Security

Mendix applications use a layered security model with three levels: project security settings, role definitions, and access rules. MDL provides complete control over all three layers.

Security Levels

The project security level determines how strictly the runtime enforces access rules.

LevelMDL KeywordDescription
OffOFFNo security enforcement (development only)
PrototypePROTOTYPESecurity enforced but incomplete configurations allowed
ProductionPRODUCTIONFull enforcement, all access rules must be complete
ALTER PROJECT SECURITY LEVEL PRODUCTION;

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

MDL provides several commands for viewing the current security configuration:

-- Project-wide settings
SHOW PROJECT SECURITY;

-- Roles
SHOW MODULE ROLES;
SHOW MODULE ROLES IN Shop;
SHOW USER ROLES;

-- Access rules
SHOW ACCESS ON MICROFLOW Shop.ACT_ProcessOrder;
SHOW ACCESS ON PAGE Shop.Order_Edit;
SHOW ACCESS ON Shop.Customer;

-- Full matrix
SHOW SECURITY MATRIX;
SHOW SECURITY MATRIX IN Shop;

-- Demo users
SHOW DEMO USERS;

Modifying Project Security

Toggle the security level and demo user visibility:

ALTER PROJECT SECURITY LEVEL PRODUCTION;
ALTER PROJECT SECURITY DEMO USERS ON;
ALTER PROJECT SECURITY DEMO USERS OFF;

See Also

Module Roles and User Roles

Mendix security uses a two-tier role system. Module roles define permissions within a single module. User roles aggregate module roles across the entire project.

Module Roles

A module role represents a set of permissions within a module. Entity access rules, microflow access, and page access are all granted to module roles.

CREATE MODULE ROLE

CREATE MODULE ROLE <Module>.<Role> [DESCRIPTION '<text>'];

Examples:

CREATE MODULE ROLE Shop.Admin DESCRIPTION 'Full administrative access';
CREATE MODULE ROLE Shop.User DESCRIPTION 'Standard customer-facing role';
CREATE MODULE ROLE Shop.Viewer;

DROP MODULE ROLE

DROP MODULE ROLE Shop.Viewer;

Listing Module Roles

SHOW MODULE ROLES;
SHOW MODULE ROLES IN Shop;

User Roles

A user role is a project-level role assigned to end users at login. Each user role includes one or more module roles, granting the user the combined permissions of all included module roles.

CREATE USER ROLE

CREATE USER ROLE <Name> (<Module>.<Role> [, ...]) [MANAGE ALL ROLES];

The MANAGE ALL ROLES option allows users with this role to assign any user role to other users (typically for administrators).

Examples:

-- Administrator with management rights
CREATE USER ROLE AppAdmin (Shop.Admin, System.Administrator) MANAGE ALL ROLES;

-- Regular user
CREATE USER ROLE AppUser (Shop.User);

-- Read-only viewer
CREATE USER ROLE AppViewer (Shop.Viewer);

ALTER USER ROLE

Add or remove module roles from an existing user role:

ALTER USER ROLE AppAdmin ADD MODULE ROLES (Reporting.Admin);
ALTER USER ROLE AppUser REMOVE MODULE ROLES (Shop.Viewer);

DROP USER ROLE

DROP USER ROLE AppViewer;

Listing User Roles

SHOW USER ROLES;

Typical Setup

A common pattern is to create module roles first, then compose them into user roles:

-- 1. Module roles
CREATE MODULE ROLE Shop.Admin DESCRIPTION 'Full shop access';
CREATE MODULE ROLE Shop.User DESCRIPTION 'Standard shop access';
CREATE MODULE ROLE Reporting.Viewer DESCRIPTION 'View reports';

-- 2. User roles
CREATE USER ROLE Administrator (Shop.Admin, Reporting.Viewer, System.Administrator) MANAGE ALL ROLES;
CREATE USER ROLE Employee (Shop.User, Reporting.Viewer);
CREATE USER ROLE Guest (Shop.User);

See Also

Entity Access

Entity access rules control which module roles can create, read, write, and delete objects of a given entity. Rules can restrict access to specific attributes and apply XPath constraints to limit which rows are visible.

GRANT on Entities

GRANT <Module>.<Role> ON <Module>.<Entity> (<rights>) [WHERE '<xpath>'];

The <rights> list is a comma-separated combination of:

RightSyntaxDescription
CreateCREATEAllow creating new objects
DeleteDELETEAllow deleting objects
Read allREAD *Read access to all attributes and associations
Read specificREAD (<attr>, ...)Read access to listed members only
Write allWRITE *Write access to all attributes and associations
Write specificWRITE (<attr>, ...)Write access to listed members only

Examples

Full Access

Grant all operations on all members:

GRANT Shop.Admin ON Shop.Customer (CREATE, DELETE, READ *, WRITE *);

Read-Only Access

GRANT Shop.Viewer ON Shop.Customer (READ *);

Selective Member Access

Restrict read and write to specific attributes:

GRANT Shop.User ON Shop.Customer (READ (Name, Email, Status), WRITE (Email));

XPath Constraints

Limit which objects a role can see or modify using an XPath expression in the WHERE clause:

-- Users can only access their own orders
GRANT Shop.User ON Shop.Order (READ *, WRITE *)
  WHERE '[Sales.Order_Customer/Sales.Customer/Name = $currentUser]';

-- Only open orders are editable
GRANT Shop.User ON Shop.Order (READ *, WRITE *)
  WHERE '[Status = ''Open'']';

Note that single quotes inside XPath expressions must be doubled (''), since the entire expression is wrapped in single quotes.

Multiple Roles on the Same Entity

Each GRANT creates a separate access rule. An entity can have rules for multiple roles:

GRANT Shop.Admin ON Shop.Order (CREATE, DELETE, READ *, WRITE *);
GRANT Shop.User ON Shop.Order (READ *, WRITE *) WHERE '[Status = ''Open'']';
GRANT Shop.Viewer ON Shop.Order (READ *);

REVOKE on Entities

Remove an entity access rule for a role:

REVOKE <Module>.<Role> ON <Module>.<Entity>;

Example:

REVOKE Shop.Viewer ON Shop.Customer;

This removes the entire access rule for that role on that entity.

Viewing Entity Access

-- See which roles have access to an entity
SHOW ACCESS ON Shop.Customer;

-- Full matrix across a module
SHOW SECURITY MATRIX IN Shop;

See Also

Microflow, Page, and Nanoflow Access

Document-level access controls which module roles can execute microflows and nanoflows or view pages. Without an explicit grant, a document is inaccessible to all roles.

Microflow Access

GRANT EXECUTE ON MICROFLOW

GRANT EXECUTE ON MICROFLOW <Module>.<Name> TO <Module>.<Role> [, ...];

Examples:

-- Single role
GRANT EXECUTE ON MICROFLOW Shop.ACT_ProcessOrder TO Shop.Admin;

-- Multiple roles
GRANT EXECUTE ON MICROFLOW Shop.ACT_ViewOrders TO Shop.User, Shop.Admin;

REVOKE EXECUTE ON MICROFLOW

REVOKE EXECUTE ON MICROFLOW <Module>.<Name> FROM <Module>.<Role> [, ...];

Example:

REVOKE EXECUTE ON MICROFLOW Shop.ACT_ProcessOrder FROM Shop.User;

Page Access

GRANT VIEW ON PAGE

GRANT VIEW ON PAGE <Module>.<Name> TO <Module>.<Role> [, ...];

Examples:

GRANT VIEW ON PAGE Shop.Order_Overview TO Shop.User, Shop.Admin;
GRANT VIEW ON PAGE Shop.Admin_Dashboard TO Shop.Admin;

REVOKE VIEW ON PAGE

REVOKE VIEW ON PAGE <Module>.<Name> FROM <Module>.<Role> [, ...];

Example:

REVOKE VIEW ON PAGE Shop.Admin_Dashboard FROM Shop.User;

Nanoflow Access

Nanoflow access uses the same syntax as microflow access:

GRANT EXECUTE ON NANOFLOW Shop.NAV_Filter TO Shop.User, Shop.Admin;
REVOKE EXECUTE ON NANOFLOW Shop.NAV_Filter FROM Shop.User;

Viewing Document Access

SHOW ACCESS ON MICROFLOW Shop.ACT_ProcessOrder;
SHOW ACCESS ON PAGE Shop.Order_Overview;

Typical Pattern

After creating documents, grant access as part of the same script:

-- Create the microflow
CREATE MICROFLOW Shop.ACT_CreateOrder
BEGIN
  DECLARE $Order Shop.Order;
  $Order = CREATE Shop.Order (Status = 'Draft');
  COMMIT $Order;
  RETURN $Order;
END;

-- Grant access
GRANT EXECUTE ON MICROFLOW Shop.ACT_CreateOrder TO Shop.User, Shop.Admin;

-- Create the page
CREATE PAGE Shop.Order_Edit (
  Params: { $Order: Shop.Order },
  Title: 'Edit Order',
  Layout: Atlas_Core.PopupLayout
) { ... }

-- Grant access
GRANT VIEW ON PAGE Shop.Order_Edit TO Shop.User, Shop.Admin;

See Also

GRANT / REVOKE

The GRANT and REVOKE statements control all permissions in a Mendix project. They work on three targets: entities (CRUD access), microflows (execute access), and pages (view access).

Entity Access

GRANT

GRANT <Module>.<Role> ON <Module>.<Entity> (<rights>) [WHERE '<xpath>'];

Where <rights> is a comma-separated list of:

RightDescription
CREATEAllow creating new objects
DELETEAllow deleting objects
READ *Read all members
READ (<attr>, ...)Read specific members only
WRITE *Write all members
WRITE (<attr>, ...)Write specific members only

Examples:

-- Full access
GRANT Shop.Admin ON Shop.Customer (CREATE, DELETE, READ *, WRITE *);

-- Read-only
GRANT Shop.Viewer ON Shop.Customer (READ *);

-- Selective members
GRANT Shop.User ON Shop.Customer (READ (Name, Email), WRITE (Email));

-- With XPath constraint (doubled single quotes for string literals)
GRANT Shop.User ON Shop.Order (READ *, WRITE *)
  WHERE '[Status = ''Open'']';

REVOKE

Remove an entity access rule entirely:

REVOKE <Module>.<Role> ON <Module>.<Entity>;

Example:

REVOKE Shop.Viewer ON Shop.Customer;

Microflow Access

GRANT EXECUTE ON MICROFLOW

GRANT EXECUTE ON MICROFLOW <Module>.<Name> TO <Module>.<Role> [, ...];

Example:

GRANT EXECUTE ON MICROFLOW Shop.ACT_ProcessOrder TO Shop.User, Shop.Admin;

REVOKE EXECUTE ON MICROFLOW

REVOKE EXECUTE ON MICROFLOW <Module>.<Name> FROM <Module>.<Role> [, ...];

Example:

REVOKE EXECUTE ON MICROFLOW Shop.ACT_ProcessOrder FROM Shop.User;

Page Access

GRANT VIEW ON PAGE

GRANT VIEW ON PAGE <Module>.<Name> TO <Module>.<Role> [, ...];

Example:

GRANT VIEW ON PAGE Shop.Order_Overview TO Shop.User, Shop.Admin;

REVOKE VIEW ON PAGE

REVOKE VIEW ON PAGE <Module>.<Name> FROM <Module>.<Role> [, ...];

Example:

REVOKE VIEW ON PAGE Shop.Admin_Dashboard FROM Shop.User;

Nanoflow Access

GRANT EXECUTE ON NANOFLOW <Module>.<Name> TO <Module>.<Role> [, ...];
REVOKE EXECUTE ON NANOFLOW <Module>.<Name> FROM <Module>.<Role> [, ...];

Workflow Access

GRANT EXECUTE ON WORKFLOW <Module>.<Name> TO <Module>.<Role> [, ...];
REVOKE EXECUTE ON WORKFLOW <Module>.<Name> FROM <Module>.<Role> [, ...];

OData Service Access

GRANT ACCESS ON ODATA SERVICE <Module>.<Name> TO <Module>.<Role> [, ...];
REVOKE ACCESS ON ODATA SERVICE <Module>.<Name> FROM <Module>.<Role> [, ...];

Complete Example

A typical security setup script:

-- Module roles
CREATE MODULE ROLE Shop.Admin DESCRIPTION 'Full access';
CREATE MODULE ROLE Shop.User DESCRIPTION 'Standard access';
CREATE MODULE ROLE Shop.Viewer DESCRIPTION 'Read-only access';

-- User roles
CREATE USER ROLE Administrator (Shop.Admin, System.Administrator) MANAGE ALL ROLES;
CREATE USER ROLE Employee (Shop.User);
CREATE USER ROLE Guest (Shop.Viewer);

-- Entity access
GRANT Shop.Admin ON Shop.Customer (CREATE, DELETE, READ *, WRITE *);
GRANT Shop.User ON Shop.Customer (READ *, WRITE (Email, Phone));
GRANT Shop.Viewer ON Shop.Customer (READ *);

GRANT Shop.Admin ON Shop.Order (CREATE, DELETE, READ *, WRITE *);
GRANT Shop.User ON Shop.Order (CREATE, READ *, WRITE *)
  WHERE '[Status = ''Open'']';
GRANT Shop.Viewer ON Shop.Order (READ *);

-- Microflow access
GRANT EXECUTE ON MICROFLOW Shop.ACT_ProcessOrder TO Shop.Admin;
GRANT EXECUTE ON MICROFLOW Shop.ACT_CreateOrder TO Shop.User, Shop.Admin;
GRANT EXECUTE ON MICROFLOW Shop.ACT_ViewOrders TO Shop.User, Shop.Admin, Shop.Viewer;

-- Page access
GRANT VIEW ON PAGE Shop.Order_Overview TO Shop.User, Shop.Admin, Shop.Viewer;
GRANT VIEW ON PAGE Shop.Order_Edit TO Shop.User, Shop.Admin;
GRANT VIEW ON PAGE Shop.Admin_Dashboard TO Shop.Admin;

-- Demo users
CREATE DEMO USER 'demo_admin' PASSWORD 'Admin123!' (Administrator);
CREATE DEMO USER 'demo_user' PASSWORD 'User123!' (Employee);

-- Enable demo users
ALTER PROJECT SECURITY DEMO USERS ON;
ALTER PROJECT SECURITY LEVEL PROTOTYPE;

See Also

Demo Users

Demo users are test accounts created for development and testing. They appear on the login screen when demo users are enabled, allowing quick access without manual user setup.

CREATE DEMO USER

CREATE DEMO USER '<username>' PASSWORD '<password>'
  [ENTITY <Module>.<Entity>]
  (<UserRole> [, ...]);
ParameterDescription
<username>Login name for the demo user
<password>Password (visible in development only)
ENTITYOptional. The entity that generalizes System.User (e.g., Administration.Account). If omitted, the system auto-detects the unique System.User subtype.
<UserRole>One or more project-level user roles to assign

Examples

-- Basic demo user
CREATE DEMO USER 'demo_admin' PASSWORD 'Admin123!' (Administrator);

-- With explicit entity
CREATE DEMO USER 'demo_admin' PASSWORD 'Admin123!'
  ENTITY Administration.Account (Administrator);

-- Multiple roles
CREATE DEMO USER 'demo_manager' PASSWORD 'Manager1!'
  (Manager, Reporting);

-- Standard user
CREATE DEMO USER 'demo_user' PASSWORD 'User1234!' (Employee);

DROP DEMO USER

DROP DEMO USER '<username>';

Example:

DROP DEMO USER 'demo_admin';

Enabling Demo Users

Demo users only appear on the login screen when enabled in project security:

ALTER PROJECT SECURITY DEMO USERS ON;

To hide them:

ALTER PROJECT SECURITY DEMO USERS OFF;

Listing Demo Users

SHOW DEMO USERS;

Typical Setup

-- Enable demo users and set prototype security
ALTER PROJECT SECURITY LEVEL PROTOTYPE;
ALTER PROJECT SECURITY DEMO USERS ON;

-- Create demo accounts for each role
CREATE DEMO USER 'demo_admin' PASSWORD 'Admin123!'
  ENTITY Administration.Account (Administrator);
CREATE DEMO USER 'demo_user' PASSWORD 'User1234!'
  ENTITY Administration.Account (Employee);
CREATE DEMO USER 'demo_guest' PASSWORD 'Guest123!'
  ENTITY Administration.Account (Guest);

See Also

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.

A Mendix application has one or more navigation profiles, each targeting a device type. MDL supports inspecting and replacing entire profiles.

Inspecting Navigation

-- Summary of all profiles
SHOW NAVIGATION;

-- Menu tree for a specific profile
SHOW NAVIGATION MENU Responsive;

-- Home page assignments across all profiles
SHOW NAVIGATION HOMES;

-- Full MDL output (round-trippable)
DESCRIBE NAVIGATION;
DESCRIBE NAVIGATION Responsive;
ProfileDescription
ResponsiveWeb browser (desktop, laptop)
TabletTablet browser
PhonePhone browser
NativePhoneNative mobile (React Native)

See Navigation Profiles for full syntax.

Settings Overview

Project settings control runtime configuration, database connections, languages, and workflow behavior.

-- Overview of all settings
SHOW SETTINGS;

-- Full MDL output
DESCRIBE SETTINGS;

See Project Settings for the ALTER SETTINGS syntax.

See Also

Navigation Profiles

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

ProfileMDL NameDescription
Responsive webResponsiveDefault browser navigation
Tablet webTabletTablet-optimized browser navigation
Phone webPhonePhone-optimized browser navigation
Native mobileNativePhoneReact Native mobile navigation

CREATE OR REPLACE NAVIGATION

Replaces an entire navigation profile:

CREATE OR REPLACE NAVIGATION <Profile>
  HOME PAGE <Module>.<Page>
  [HOME PAGE <Module>.<Page> FOR <Module>.<Role>]
  [LOGIN PAGE <Module>.<Page>]
  [NOT FOUND PAGE <Module>.<Page>]
  [MENU (
    <menu-items>
  )]

Full Example

CREATE OR REPLACE NAVIGATION Responsive
  HOME PAGE MyModule.Home_Web
  HOME PAGE MyModule.AdminHome FOR MyModule.Administrator
  LOGIN PAGE Administration.Login
  NOT FOUND PAGE MyModule.Custom404
  MENU (
    MENU ITEM 'Home' PAGE MyModule.Home_Web;
    MENU ITEM 'Orders' PAGE Shop.Order_Overview;
    MENU 'Administration' (
      MENU ITEM 'Users' PAGE Administration.Account_Overview;
      MENU ITEM 'Settings' PAGE MyModule.Settings;
    );
  );

Minimal Example

A profile only requires a home page:

CREATE OR REPLACE NAVIGATION Phone
  HOME PAGE MyModule.Home_Phone;

DESCRIBE NAVIGATION

View the current navigation profile in MDL syntax:

-- All profiles
DESCRIBE NAVIGATION;

-- Single profile
DESCRIBE NAVIGATION Responsive;

The output is round-trippable – you can copy it, modify it, and execute it as a CREATE OR REPLACE NAVIGATION statement.

See Also

Home Pages and Menus

Each navigation profile has a default home page, optional role-specific home pages, and a menu tree. These determine what users see when they first open the application and how they navigate between pages.

Home Pages

Default Home Page

Every profile requires a default home page. This is shown to users whose role has no specific home page assignment:

CREATE OR REPLACE NAVIGATION Responsive
  HOME PAGE MyModule.Home_Web;

Role-Specific Home Pages

Use HOME PAGE ... FOR to direct users to different pages based on their module role:

CREATE OR REPLACE NAVIGATION Responsive
  HOME PAGE MyModule.Home_Web
  HOME PAGE MyModule.AdminDashboard FOR MyModule.Administrator
  HOME PAGE MyModule.ManagerDashboard FOR MyModule.Manager;

When a user logs in, the runtime checks their roles and redirects to the most specific matching home page. If no role-specific page matches, the default home page is used.

Login Page

The login page is shown to unauthenticated users:

CREATE OR REPLACE NAVIGATION Responsive
  HOME PAGE MyModule.Home_Web
  LOGIN PAGE Administration.Login;

Not Found Page

An optional custom 404 page:

CREATE OR REPLACE NAVIGATION Responsive
  HOME PAGE MyModule.Home_Web
  NOT FOUND PAGE MyModule.Custom404;

The MENU block defines the navigation menu as a tree of items and submenus.

A menu item links a label to a page:

MENU ITEM '<label>' PAGE <Module>.<Page>;

Nest items inside a MENU '<label>' (...) block:

MENU '<label>' (
  <menu-items>
);

Complete Example

CREATE OR REPLACE NAVIGATION Responsive
  HOME PAGE Shop.Home
  LOGIN PAGE Administration.Login
  MENU (
    MENU ITEM 'Home' PAGE Shop.Home;
    MENU ITEM 'Products' PAGE Shop.Product_Overview;
    MENU ITEM 'Orders' PAGE Shop.Order_Overview;
    MENU 'Administration' (
      MENU ITEM 'Users' PAGE Administration.Account_Overview;
      MENU ITEM 'Roles' PAGE Administration.Role_Overview;
      MENU 'System' (
        MENU ITEM 'Logs' PAGE Administration.Log_Overview;
        MENU ITEM 'Settings' PAGE Shop.Settings;
      );
    );
  );

Inspecting Menus

-- View menu tree
SHOW NAVIGATION MENU;
SHOW NAVIGATION MENU Responsive;

-- View home page assignments
SHOW NAVIGATION HOMES;

See Also

Project Settings

Project settings control application runtime behavior, server configurations, language settings, and workflow configuration. MDL provides commands to inspect and modify all settings categories.

Inspecting Settings

-- Overview of all settings
SHOW SETTINGS;

-- Full MDL output (round-trippable)
DESCRIBE SETTINGS;

ALTER SETTINGS

Settings are organized into categories. Each ALTER SETTINGS command targets one category.

Model Settings

Runtime-level settings such as the after-startup microflow, hashing algorithm, and Java version:

ALTER SETTINGS MODEL <Key> = <Value>;

Examples:

ALTER SETTINGS MODEL AfterStartupMicroflow = 'MyModule.ACT_Startup';
ALTER SETTINGS MODEL HashAlgorithm = 'BCrypt';
ALTER SETTINGS MODEL JavaVersion = '17';

Configuration Settings

Server configuration settings like database type, URL, and HTTP port. Each configuration is identified by name (commonly 'default'):

ALTER SETTINGS CONFIGURATION '<Name>' <Key> = <Value>;

Examples:

ALTER SETTINGS CONFIGURATION 'default' DatabaseType = 'POSTGRESQL';
ALTER SETTINGS CONFIGURATION 'default' DatabaseUrl = 'jdbc:postgresql://localhost:5432/myapp';
ALTER SETTINGS CONFIGURATION 'default' HttpPortNumber = '8080';

Constant Overrides

Override a constant value within a specific configuration:

ALTER SETTINGS CONSTANT '<ConstantName>' VALUE '<value>' IN CONFIGURATION '<cfg>';

Example:

ALTER SETTINGS CONSTANT 'MyModule.ApiBaseUrl' VALUE 'https://staging.example.com' IN CONFIGURATION 'default';

Language Settings

Set the default language for the application:

ALTER SETTINGS LANGUAGE <Key> = <Value>;

Example:

ALTER SETTINGS LANGUAGE DefaultLanguageCode = 'en_US';

Workflow Settings

Configure workflow behavior such as the user entity and task parallelism:

ALTER SETTINGS WORKFLOWS <Key> = <Value>;

Examples:

ALTER SETTINGS WORKFLOWS UserEntity = 'Administration.Account';
ALTER SETTINGS WORKFLOWS DefaultTaskParallelism = '5';

See Also

Workflows

Workflows model long-running, multi-step business processes such as approval chains, onboarding sequences, and review cycles. Unlike microflows, which execute synchronously in a single transaction, workflows persist their state and can pause indefinitely – waiting for user input, timers, or external notifications.

When to Use Workflows

Workflows are appropriate when a process:

  • Involves human tasks that require user action at specific steps
  • Spans hours, days, or weeks rather than completing instantly
  • Has branching logic based on user decisions (approve / reject / escalate)
  • Needs a visual overview of progress for administrators

For immediate, transactional logic, use microflows instead. See Workflow vs Microflow for a detailed comparison.

Inspecting Workflows

-- List all workflows
SHOW WORKFLOWS;
SHOW WORKFLOWS IN MyModule;

-- View full definition
DESCRIBE WORKFLOW MyModule.ApprovalFlow;

Quick Example

CREATE WORKFLOW Approval.RequestApproval
  PARAMETER $Context: Approval.Request
  OVERVIEW PAGE Approval.WorkflowOverview
BEGIN
  USER TASK ReviewTask 'Review the request'
    PAGE Approval.ReviewPage
    OUTCOMES 'Approve' {
      CALL MICROFLOW Approval.ACT_Approve;
    } 'Reject' {
      CALL MICROFLOW Approval.ACT_Reject;
    };
END WORKFLOW;

See Also

Workflow Structure

A workflow definition consists of a context parameter, an optional overview page, and a sequence of activities. Activities execute in order unless control flow elements (decisions, parallel splits, jumps) alter the sequence.

CREATE WORKFLOW

CREATE [OR MODIFY] WORKFLOW <Module>.<Name>
  PARAMETER $<Name>: <Module>.<Entity>
  [OVERVIEW PAGE <Module>.<Page>]
BEGIN
  <activities>
END WORKFLOW;
ElementDescription
PARAMETERThe workflow context object, passed when the workflow is started
OVERVIEW PAGEOptional page shown in the workflow admin dashboard
ActivitiesA sequence of user tasks, microflow calls, decisions, etc.

Full Example

CREATE WORKFLOW HR.OnboardEmployee
  PARAMETER $Context: HR.OnboardingRequest
  OVERVIEW PAGE HR.OnboardingOverview
BEGIN
  -- Parallel: IT setup and HR paperwork happen simultaneously
  PARALLEL SPLIT
    PATH 1 {
      USER TASK SetupLaptop 'Set up laptop and accounts'
        PAGE HR.IT_SetupPage
        OUTCOMES 'Done' { };
    }
    PATH 2 {
      USER TASK SignDocuments 'Sign employment documents'
        PAGE HR.DocumentSignPage
        OUTCOMES 'Signed' { };
    };

  -- After both paths complete, manager reviews
  DECISION 'Manager approval required?'
    OUTCOMES 'Yes' {
      USER TASK ManagerReview 'Review onboarding'
        PAGE HR.ManagerReviewPage
        OUTCOMES 'Approve' { } 'Reject' {
          CALL MICROFLOW HR.ACT_RejectOnboarding;
          END;
        };
    } 'No' { };

  CALL MICROFLOW HR.ACT_CompleteOnboarding;
END WORKFLOW;

DROP WORKFLOW

DROP WORKFLOW HR.OnboardEmployee;

Workflow Access

Grant or revoke execute access to control who can start a workflow:

GRANT EXECUTE ON WORKFLOW HR.OnboardEmployee TO HR.Manager, HR.Admin;
REVOKE EXECUTE ON WORKFLOW HR.OnboardEmployee FROM HR.Manager;

See Also

Activity Types

Workflow activities are the building blocks of a workflow definition. Each activity type serves a different purpose, from waiting for user input to calling microflows or branching execution.

User Task

A user task pauses the workflow until a user completes it. Each outcome resumes the workflow down a different path.

USER TASK <name> '<caption>'
  [PAGE <Module>.<Page>]
  [TARGETING MICROFLOW <Module>.<Microflow>]
  OUTCOMES '<outcome>' { <activities> } ['<outcome>' { <activities> }] ...;
ElementDescription
<name>Internal activity name
<caption>Display label shown to users
PAGEThe page opened when the user acts on the task
TARGETING MICROFLOWMicroflow that determines which users see the task
OUTCOMESNamed outcomes, each with a block of follow-up activities

Example:

USER TASK ReviewTask 'Review the request'
  PAGE Approval.ReviewPage
  TARGETING MICROFLOW Approval.ACT_GetReviewers
  OUTCOMES 'Approve' {
    CALL MICROFLOW Approval.ACT_Approve;
  } 'Reject' {
    CALL MICROFLOW Approval.ACT_Reject;
    END;
  };

Call Microflow

Execute a microflow as part of the workflow. Optionally specify a comment and outcomes:

CALL MICROFLOW <Module>.<Name> [COMMENT '<text>']
  [OUTCOMES '<outcome>' { <activities> } ...];

Example:

CALL MICROFLOW HR.ACT_SendNotification COMMENT 'Notify the applicant';

Call Workflow

Start a sub-workflow:

CALL WORKFLOW <Module>.<Name> [COMMENT '<text>'];

Example:

CALL WORKFLOW HR.BackgroundCheck COMMENT 'Run background check sub-process';

Decision

Branch the workflow based on a condition. Each outcome contains a block of activities:

DECISION ['<caption>']
  OUTCOMES '<outcome>' { <activities> } ['<outcome>' { <activities> }] ...;

Example:

DECISION 'Order value over $1000?'
  OUTCOMES 'Yes' {
    USER TASK ManagerApproval 'Manager must approve'
      PAGE Shop.ApprovalPage
      OUTCOMES 'Approved' { } 'Rejected' { END; };
  } 'No' { };

Parallel Split

Execute multiple paths concurrently. The workflow continues after all paths complete:

PARALLEL SPLIT
  PATH 1 { <activities> }
  PATH 2 { <activities> }
  [PATH 3 { <activities> }] ...;

Example:

PARALLEL SPLIT
  PATH 1 {
    USER TASK LegalReview 'Legal review'
      PAGE Legal.ReviewPage
      OUTCOMES 'Approved' { };
  }
  PATH 2 {
    USER TASK FinanceReview 'Finance review'
      PAGE Finance.ReviewPage
      OUTCOMES 'Approved' { };
  };

Jump To

Jump to a named activity elsewhere in the workflow (creates a loop or skip):

JUMP TO <activity-name>;

Example:

JUMP TO ReviewTask;

Wait for Timer

Pause the workflow until a timer expression evaluates:

WAIT FOR TIMER ['<expression>'];

Example:

WAIT FOR TIMER 'addDays([%CurrentDateTime%], 3)';

Wait for Notification

Pause the workflow until an external notification resumes it:

WAIT FOR NOTIFICATION;

End

Terminate the current workflow path:

END;

Typically used inside an outcome block to stop the workflow after a rejection or cancellation.

Summary Table

ActivityPurposePauses Workflow?
USER TASKWait for human actionYes
CALL MICROFLOWExecute server logicNo
CALL WORKFLOWStart sub-workflowDepends on sub-workflow
DECISIONBranch on conditionNo
PARALLEL SPLITConcurrent executionYes (waits for all paths)
JUMP TOGo to named activityNo
WAIT FOR TIMERDelay executionYes
WAIT FOR NOTIFICATIONWait for external signalYes
ENDTerminate pathN/A

See Also

Workflow vs Microflow

Workflows and microflows are both used to express business logic, but they serve different purposes and have different execution models.

Comparison

AspectMicroflowWorkflow
DurationMilliseconds to secondsHours, days, or weeks
ExecutionSynchronous, single transactionAsynchronous, persisted state
TriggerButton click, page load, eventExplicitly started, resumes on events
Human tasksNot supported (shows pages but does not wait)Built-in user task with outcomes
BranchingIF / ELSE within a single executionDecisions with separate outcome paths
ParallelismNot supportedParallel splits with multiple concurrent paths
PersistenceNo state between callsState persisted across restarts
MonitoringLog messages onlyAdmin overview page, task inbox
Error handlingON ERROR blocksOutcome-based routing

When to Use a Microflow

Use a microflow when:

  • The operation completes immediately (create, update, delete, calculate)
  • No human decision points are needed
  • The logic runs within a single database transaction
  • You need fine-grained error handling with rollback
CREATE MICROFLOW Shop.ACT_ProcessPayment
BEGIN
  DECLARE $Order Shop.Order;
  RETRIEVE $Order FROM Shop.Order WHERE [OrderId = $OrderId] LIMIT 1;
  CHANGE $Order (Status = 'Paid', PaidDate = [%CurrentDateTime%]);
  COMMIT $Order;
  RETURN true;
END;

When to Use a Workflow

Use a workflow when:

  • The process involves human approval or review steps
  • The process spans multiple days or longer
  • You need a visual overview of where each case stands
  • Multiple steps should happen in parallel
  • The process may wait for external events
CREATE WORKFLOW Shop.OrderApproval
  PARAMETER $Context: Shop.Order
  OVERVIEW PAGE Shop.OrderWorkflowOverview
BEGIN
  USER TASK ReviewOrder 'Review the order'
    PAGE Shop.OrderReviewPage
    OUTCOMES 'Approve' {
      CALL MICROFLOW Shop.ACT_ApproveOrder;
    } 'Reject' {
      CALL MICROFLOW Shop.ACT_RejectOrder;
      END;
    };
  CALL MICROFLOW Shop.ACT_FulfillOrder;
END WORKFLOW;

Combining Both

Workflows and microflows work together. A workflow orchestrates the process while microflows handle the actual logic at each step:

  1. Workflow defines the process: who needs to act, in what order, with what outcomes
  2. Microflows execute within workflow steps: create objects, send emails, update status
  3. Pages provide the UI for user tasks

A common pattern is:

-- Microflow does the work
CREATE MICROFLOW Approval.ACT_Approve
BEGIN
  DECLARE $Request Approval.Request;
  RETRIEVE $Request FROM Approval.Request WHERE [id = $RequestId] LIMIT 1;
  CHANGE $Request (Status = 'Approved', ApprovedBy = '[%CurrentUser%]');
  COMMIT $Request;
END;

-- Workflow orchestrates the process
CREATE WORKFLOW Approval.ApprovalProcess
  PARAMETER $Context: Approval.Request
BEGIN
  USER TASK Review 'Review request'
    PAGE Approval.ReviewPage
    OUTCOMES 'Approve' {
      CALL MICROFLOW Approval.ACT_Approve;
    } 'Reject' {
      CALL MICROFLOW Approval.ACT_Reject;
      END;
    };
END WORKFLOW;

See Also

Business Events

Business events enable event-driven integration between Mendix applications. An application can publish events when something happens (e.g., an order is placed) and other applications can subscribe to those events and react accordingly.

Business events are organized into event services, each of which defines one or more messages with typed attributes.

Inspecting Business Events

-- List all business event services
SHOW BUSINESS EVENTS;
SHOW BUSINESS EVENTS IN MyModule;

-- View full definition
DESCRIBE BUSINESS EVENT SERVICE MyModule.OrderEvents;

Quick Example

CREATE BUSINESS EVENT SERVICE Shop.OrderEvents (
  Version: '1.0',
  Description: 'Events related to order lifecycle'
) {
  MESSAGE OrderPlaced (
    OrderId: String,
    CustomerName: String,
    TotalAmount: Decimal,
    OrderDate: DateTime
  )
  MESSAGE OrderShipped (
    OrderId: String,
    TrackingNumber: String,
    ShippedDate: DateTime
  )
};

See Also

Event Services

An event service is a named container for business event messages. It defines the contract between publishers and consumers: what events exist and what data each event carries.

CREATE BUSINESS EVENT SERVICE

CREATE BUSINESS EVENT SERVICE <Module>.<Name> (
  [Version: '<version>',]
  [Description: '<text>']
) {
  MESSAGE <MessageName> (
    <AttributeName>: <Type> [, ...]
  )
  [MESSAGE <MessageName> ( ... )]
};
ElementDescription
VersionService version string
DescriptionHuman-readable description of the service
MESSAGEA named event type with typed attributes

Attribute Types

Message attributes support standard Mendix types:

TypeDescription
StringText value
Integer32-bit integer
Long64-bit integer
DecimalPrecise decimal number
BooleanTrue or false
DateTimeDate and time value

Example

CREATE BUSINESS EVENT SERVICE HR.EmployeeEvents (
  Version: '2.0',
  Description: 'Employee lifecycle events'
) {
  MESSAGE EmployeeHired (
    EmployeeId: String,
    FullName: String,
    Department: String,
    StartDate: DateTime
  )
  MESSAGE EmployeeTerminated (
    EmployeeId: String,
    TerminationDate: DateTime,
    Reason: String
  )
};

DROP BUSINESS EVENT SERVICE

Remove an event service:

DROP BUSINESS EVENT SERVICE HR.EmployeeEvents;

Listing and Inspecting

-- List all services
SHOW BUSINESS EVENTS;

-- Filter by module
SHOW BUSINESS EVENTS IN HR;

-- Full MDL definition
DESCRIBE BUSINESS EVENT SERVICE HR.EmployeeEvents;

The DESCRIBE output is round-trippable – it can be copied, modified, and executed as a CREATE statement.

See Also

Publishing and Consuming Events

Business events follow a publish-subscribe pattern. One application publishes events when significant actions occur, and other applications consume those events to trigger their own logic.

Publishing Events

A publishing application defines a business event service with its message types:

CREATE BUSINESS EVENT SERVICE Shop.OrderEvents (
  Version: '1.0',
  Description: 'Order lifecycle events'
) {
  MESSAGE OrderPlaced (
    OrderId: String,
    CustomerName: String,
    TotalAmount: Decimal,
    OrderDate: DateTime
  )
  MESSAGE OrderCancelled (
    OrderId: String,
    Reason: String,
    CancelledDate: DateTime
  )
};

The event service acts as a contract. Consuming applications rely on the message structure, so changes to attribute names or types should be accompanied by a version increment.

Consuming Events

A consuming application subscribes to events from another application’s event service. When an event is received, a configured microflow is triggered to handle it.

The consumption side is typically configured in Mendix Studio Pro by selecting the published event service and mapping each message to a handler microflow. The handler microflow receives the event attributes as parameters.

Handler Pattern

A typical handler microflow processes the incoming event data:

CREATE MICROFLOW Warehouse.ACT_HandleOrderPlaced
BEGIN
  -- Parameters: $OrderId (String), $CustomerName (String), etc.
  DECLARE $ShipmentRequest Warehouse.ShipmentRequest;
  $ShipmentRequest = CREATE Warehouse.ShipmentRequest (
    ExternalOrderId = $OrderId,
    CustomerName = $CustomerName,
    Status = 'Pending'
  );
  COMMIT $ShipmentRequest;
END;

Versioning

Event services include a version string to track contract changes:

CREATE BUSINESS EVENT SERVICE Shop.OrderEvents (
  Version: '2.0',
  Description: 'Order events - v2 adds ShippingAddress'
) {
  MESSAGE OrderPlaced (
    OrderId: String,
    CustomerName: String,
    TotalAmount: Decimal,
    OrderDate: DateTime,
    ShippingAddress: String
  )
};

When adding new attributes, increment the version so consuming applications know to update their handlers.

Use Cases

ScenarioPublisherConsumer
Order fulfillmentShop app publishes OrderPlacedWarehouse app creates shipment request
Customer syncCRM app publishes CustomerUpdatedBilling app updates customer record
ComplianceHR app publishes EmployeeTerminatedIT app revokes access
NotificationsAny app publishes eventsNotification service sends emails/SMS

See Also

Image Collections

Image collections are Mendix’s way of bundling images (icons, logos, graphics) within a module. Each collection can contain multiple images in various formats (PNG, SVG, GIF, JPEG, BMP, WebP).

Inspecting Image Collections

-- List all image collections across all modules
SHOW IMAGE COLLECTION;

-- Filter by module
SHOW IMAGE COLLECTION IN MyModule;

-- View full definition including embedded images
DESCRIBE IMAGE COLLECTION MyModule.AppIcons;

The DESCRIBE output includes the full CREATE statement. If the collection contains images, they are shown with IMAGE 'name' FROM FILE 'path' syntax that can be copied and re-executed. In the TUI, images are rendered inline when the terminal supports it (Kitty, iTerm2, Sixel).

CREATE IMAGE COLLECTION

CREATE IMAGE COLLECTION <Module>.<Name>
  [EXPORT LEVEL 'Hidden'|'Public']
  [COMMENT '<description>']
  [(
    IMAGE <Name> FROM FILE '<path>',
    ...
  )];
OptionDescriptionDefault
EXPORT LEVEL'Hidden' (internal to module) or 'Public' (accessible from other modules)'Hidden'
COMMENTDocumentation for the collection(none)
IMAGE Name FROM FILELoad an image from the filesystem into the collection(none)

The image format is detected automatically from the file extension. Relative paths are resolved from the current working directory. Supported formats: PNG, SVG, GIF, JPEG, BMP, WebP.

Examples

-- Minimal: empty collection
CREATE IMAGE COLLECTION MyModule.AppIcons;

-- With export level
CREATE IMAGE COLLECTION MyModule.SharedIcons EXPORT LEVEL 'Public';

-- With comment
CREATE IMAGE COLLECTION MyModule.StatusIcons
  COMMENT 'Icons for order and task status indicators';

-- With images from files
CREATE IMAGE COLLECTION MyModule.NavigationIcons (
  IMAGE home FROM FILE 'assets/home.png',
  IMAGE settings FROM FILE 'assets/settings.svg'
);

-- All options combined
CREATE IMAGE COLLECTION MyModule.BrandAssets
  EXPORT LEVEL 'Public'
  COMMENT 'Company branding assets' (
  IMAGE logo_dark FROM FILE 'assets/logo-dark.png',
  IMAGE logo_light FROM FILE 'assets/logo-light.png'
);

DROP IMAGE COLLECTION

Remove a collection and all its embedded images:

DROP IMAGE COLLECTION MyModule.StatusIcons;

See Also

Code Navigation

mxcli provides a suite of cross-reference navigation commands that let you explore relationships between elements in a Mendix project. These commands answer questions like “what calls this microflow?”, “what would break if I change this entity?”, and “what does this element depend on?”

Prerequisites

Cross-reference navigation commands require a full catalog refresh before use:

REFRESH CATALOG FULL

This populates the reference data needed for callers, callees, references, impact, and context queries. A basic REFRESH CATALOG only builds metadata tables and is not sufficient for cross-reference navigation.

Available Commands

CommandPurpose
SHOW CALLERS OFFind what calls a given element
SHOW CALLEES OFFind what a given element calls
SHOW REFERENCES OFFind all references to and from an element
SHOW IMPACT OFAnalyze the impact of changing an element
SHOW CONTEXT OFShow the surrounding context of an element
SEARCHFull-text search across all strings and source

CLI Subcommands

These commands are also available as CLI subcommands for scripting and piping:

mxcli callers -p app.mpr Module.MyMicroflow
mxcli callees -p app.mpr Module.MyMicroflow
mxcli refs -p app.mpr Module.Customer
mxcli impact -p app.mpr Module.Customer
mxcli context -p app.mpr Module.MyMicroflow --depth 3
mxcli search -p app.mpr "validation"

Typical Workflow

  1. Refresh the catalog to ensure cross-reference data is current
  2. Explore references to understand how elements are connected
  3. Analyze impact before making changes
  4. Use context to gather surrounding information for AI-assisted development
-- Step 1: Build the catalog
REFRESH CATALOG FULL;

-- Step 2: Understand what uses a microflow
SHOW CALLERS OF Sales.ACT_ProcessOrder;

-- Step 3: Check impact before renaming an entity
SHOW IMPACT OF Sales.Customer;

-- Step 4: Gather context for AI consumption
SHOW CONTEXT OF Sales.SubmitOrder;

How AI Assistants Use This

When you ask an AI assistant to modify an element, it uses these commands to:

  1. Discover dependencies with SHOW CALLERS and SHOW REFERENCES
  2. Assess risk with SHOW IMPACT before making changes
  3. Gather context with SHOW CONTEXT for informed code generation
  4. Find related elements with SEARCH to understand patterns

SHOW CALLERS / CALLEES

These commands trace the call graph of your Mendix project, showing what calls a given element and what that element calls.

Prerequisites

Both commands require a full catalog refresh:

REFRESH CATALOG FULL;

SHOW CALLERS OF

Shows all elements that call or reference a given element.

Syntax:

SHOW CALLERS OF <qualified-name>

Examples:

-- Find everything that calls a microflow
SHOW CALLERS OF Sales.ACT_ProcessOrder;

-- Find what references an entity
SHOW CALLERS OF Sales.Customer;

-- Find what uses a page
SHOW CALLERS OF Sales.CustomerOverview;

CLI Usage

# Basic caller lookup
mxcli callers -p app.mpr Module.MyMicroflow

# Transitive callers (full call chain)
mxcli callers -p app.mpr Module.MyMicroflow --transitive

The --transitive flag follows the call chain recursively, showing not just direct callers but also callers of callers.

SHOW CALLEES OF

Shows all elements that a given element calls or references.

Syntax:

SHOW CALLEES OF <qualified-name>

Examples:

-- Find what a microflow calls
SHOW CALLEES OF Sales.ACT_ProcessOrder;

-- Find what entities a microflow uses
SHOW CALLEES OF Sales.SubmitOrder;

CLI Usage

mxcli callees -p app.mpr Module.MyMicroflow

Use Cases

Before Refactoring

Check what calls a microflow before renaming or modifying it:

SHOW CALLERS OF Sales.ACT_OldName;
-- Review the list of callers before making changes

Understanding Dependencies

Trace the full call chain of a complex microflow:

SHOW CALLEES OF Sales.ACT_ProcessOrder;
-- Shows: Sales.ValidateOrder, Sales.UpdateInventory, Sales.SendConfirmation

Finding Dead Code

If SHOW CALLERS returns no results, the element may be unused:

SHOW CALLERS OF Sales.ACT_UnusedMicroflow;
-- Empty result = potentially dead code

SHOW REFERENCES / IMPACT

These commands provide broader reference tracking and impact analysis, helping you understand how changes propagate through a project.

Prerequisites

Both commands require a full catalog refresh:

REFRESH CATALOG FULL;

SHOW REFERENCES OF

Shows all references to and from a given element, combining both incoming and outgoing relationships.

Syntax:

SHOW REFERENCES OF <qualified-name>

Examples:

-- Find all references to/from an entity
SHOW REFERENCES OF Sales.Customer;

-- Find all references to/from a microflow
SHOW REFERENCES OF Sales.ACT_ProcessOrder;

-- Find all references to/from a page
SHOW REFERENCES OF Sales.CustomerOverview;

CLI Usage

mxcli refs -p app.mpr Module.Customer

Difference from CALLERS/CALLEES

While SHOW CALLERS and SHOW CALLEES focus on the call graph direction, SHOW REFERENCES combines both directions into a single view. It shows every element that either references or is referenced by the target element.

SHOW IMPACT OF

Performs impact analysis on an element, showing what would be affected if you changed or removed it.

Syntax:

SHOW IMPACT OF <qualified-name>

Examples:

-- Analyze impact of changing an entity
SHOW IMPACT OF Sales.Customer;

-- Analyze impact before removing a microflow
SHOW IMPACT OF Sales.ACT_CalculateTotal;

-- Check impact before moving an element
SHOW IMPACT OF Sales.CustomerOverview;

CLI Usage

mxcli impact -p app.mpr Module.Customer

Use Cases

Pre-Change Impact Assessment

Before modifying an entity, check what would be affected:

-- What depends on the Customer entity?
SHOW IMPACT OF Sales.Customer;

-- Review results: pages, microflows, associations, access rules
-- Then decide if the change is safe to make

Before Moving Elements

Moving elements across modules changes their qualified name and can break references:

-- Check impact before moving
SHOW IMPACT OF OldModule.Customer;

-- If impact is acceptable, proceed
MOVE ENTITY OldModule.Customer TO NewModule;

Finding All Usages of an Enumeration

SHOW REFERENCES OF Sales.OrderStatus;
-- Shows: entities with attributes of this type, microflows that use it, pages that display it

Dependency Mapping

Understand the full dependency web of a complex module:

SHOW REFERENCES OF Sales.Order;
SHOW REFERENCES OF Sales.OrderLine;
SHOW REFERENCES OF Sales.Order_Customer;

SHOW CONTEXT

The SHOW CONTEXT command assembles the surrounding context of an element within its module and project hierarchy. This is particularly useful for AI-assisted development, where an LLM needs to understand the neighborhood of an element to generate accurate code.

Prerequisites

Requires a full catalog refresh:

REFRESH CATALOG FULL;

Syntax

SHOW CONTEXT OF <qualified-name>

Examples

-- Show context of a microflow
SHOW CONTEXT OF Sales.ACT_ProcessOrder;

-- Show context of an entity
SHOW CONTEXT OF Sales.Customer;

-- Show context of a page
SHOW CONTEXT OF Sales.CustomerOverview;

CLI Usage

# Basic context retrieval
mxcli context -p app.mpr Module.MyMicroflow

# With depth control
mxcli context -p app.mpr Module.MyMicroflow --depth 3

The --depth flag controls how many levels of related elements are included in the context output. A higher depth gives a broader picture but produces more output.

What Context Includes

The context command gathers information about:

  • The element’s own definition (MDL source)
  • Elements that call it (callers)
  • Elements it calls (callees)
  • Related entities and associations
  • Pages that display or edit the same data
  • The module structure surrounding the element

Use Case: AI-Assisted Development

When an AI assistant needs to modify a microflow, it uses SHOW CONTEXT to understand:

  1. What parameters the microflow expects
  2. What entities and associations are involved
  3. What other microflows it interacts with
  4. What pages display the same data

This information allows the AI to generate code that fits naturally into the existing project structure.

# Gather context for an AI prompt
mxcli context -p app.mpr Sales.ACT_ProcessOrder --depth 2

Full-Text Search

The SEARCH command performs full-text search across all strings and source code in a Mendix project. It searches element names, documentation, captions, expressions, and other string content.

Syntax

SEARCH '<keyword>'

Examples

-- Search for a keyword
SEARCH 'validation';

-- Search for an entity name
SEARCH 'Customer';

-- Search for error messages
SEARCH 'cannot be empty';

-- Search for specific patterns
SEARCH 'CurrentDateTime';

CLI Usage

# Basic search
mxcli search -p app.mpr "validation"

# Quiet mode (no headers/formatting), suitable for piping
mxcli search -p app.mpr "validation" -q

# Output as names (type<TAB>name per line)
mxcli search -p app.mpr "validation" -q --format names

# Output as JSON array
mxcli search -p app.mpr "validation" -q --format json

Output Formats

FormatDescription
names (default)type<TAB>name per line, suitable for piping
jsonJSON array of search results

The -q (quiet) flag suppresses headers and formatting, making output suitable for piping to other commands.

Piping Search Results

Search results can be piped to other mxcli commands for powerful workflows:

# Find the first microflow matching "error" and describe it
mxcli search -p app.mpr "error" -q --format names | head -1 | awk '{print $2}' | \
  xargs mxcli describe -p app.mpr microflow

What Gets Searched

The search command looks through:

  • Element names (entities, microflows, pages, etc.)
  • Documentation comments
  • Attribute names and captions
  • Expression strings in microflows
  • Widget captions and labels
  • Enumeration value captions
  • Log message strings
  • Validation feedback messages

Comparison with Catalog Queries

SEARCH provides quick keyword-based search, while catalog queries (SELECT FROM CATALOG.*) provide structured SQL-based queries with filtering, joining, and aggregation. Use SEARCH for quick discovery and catalog queries for precise analysis.

-- Quick search: find anything mentioning "Customer"
SEARCH 'Customer';

-- Precise query: find entities named Customer with their module
SELECT Name, ModuleName FROM CATALOG.ENTITIES WHERE Name LIKE '%Customer%';

Catalog Queries

The catalog is a SQLite-based query system that provides SQL access to project metadata. It indexes all elements in your Mendix project – entities, microflows, pages, associations, attributes, and more – into queryable tables that support standard SQL syntax.

How It Works

The catalog builds an in-memory SQLite database from the project’s MPR file. This database is cached in .mxcli/catalog.db next to the MPR file for fast subsequent access.

There are two levels of catalog refresh:

CommandWhat It BuildsUse Case
REFRESH CATALOGBasic metadata tables (entities, microflows, pages, etc.)Quick queries about project structure
REFRESH CATALOG FULLMetadata + cross-references + source contentCode navigation, impact analysis, full-text search

Quick Start

-- Build the catalog
REFRESH CATALOG;

-- See what tables are available
SHOW CATALOG TABLES;

-- Query entities in a module
SELECT Name, EntityType FROM CATALOG.ENTITIES WHERE ModuleName = 'Sales';

-- Find microflows that reference an entity
SELECT Name, ModuleName FROM CATALOG.MICROFLOWS
WHERE Description LIKE '%Customer%' OR Name LIKE '%Customer%';

Key Features

  • Standard SQL syntax – SELECT, WHERE, JOIN, GROUP BY, ORDER BY, HAVING, UNION
  • Multiple tables – Entities, attributes, associations, microflows, pages, and more
  • Cross-reference data – Available after REFRESH CATALOG FULL
  • Cached for performance – Stored in .mxcli/catalog.db next to the MPR file
  • AI-friendly – Structured data that AI assistants can query programmatically

CLI Usage

# Refresh catalog
mxcli -p app.mpr -c "REFRESH CATALOG"
mxcli -p app.mpr -c "REFRESH CATALOG FULL"

# Query catalog
mxcli -p app.mpr -c "SELECT Name FROM CATALOG.MICROFLOWS LIMIT 10"
mxcli -p app.mpr -c "SHOW CATALOG TABLES"

REFRESH CATALOG

The REFRESH CATALOG command builds or rebuilds the catalog index from the current project state. The catalog must be refreshed before running catalog queries or cross-reference navigation commands.

Syntax

-- Basic refresh: builds metadata tables
REFRESH CATALOG

-- Full refresh: builds metadata + cross-references + source
REFRESH CATALOG FULL

-- Force rebuild: ignores cached catalog
REFRESH CATALOG FULL FORCE

Refresh Levels

Basic Refresh

REFRESH CATALOG;

Builds the core metadata tables: entities, attributes, associations, microflows, pages, enumerations, and other element types. This is sufficient for structural queries like “list all entities in a module” or “find pages with a specific URL.”

Full Refresh

REFRESH CATALOG FULL;

Builds everything in the basic refresh plus:

  • Cross-reference data – Which elements reference which other elements
  • Source content – Microflow activity content, expression strings, widget properties
  • Call graph – Caller/callee relationships between elements

This level is required for:

  • SHOW CALLERS OF
  • SHOW CALLEES OF
  • SHOW REFERENCES OF
  • SHOW IMPACT OF
  • SHOW CONTEXT OF
  • SEARCH
  • Full-text catalog queries

Force Rebuild

REFRESH CATALOG FULL FORCE;

Ignores the cached .mxcli/catalog.db file and rebuilds from scratch. Use this when the catalog appears stale or corrupt.

CLI Usage

# Quick refresh
mxcli -p app.mpr -c "REFRESH CATALOG"

# Full refresh with cross-references
mxcli -p app.mpr -c "REFRESH CATALOG FULL"

# Force rebuild
mxcli -p app.mpr -c "REFRESH CATALOG FULL FORCE"

Caching

The catalog is cached in .mxcli/catalog.db next to the MPR file. On subsequent refreshes, mxcli checks whether the project has changed and only rebuilds if necessary (unless FORCE is specified).

When to Refresh

  • After opening a project – Run REFRESH CATALOG to enable queries
  • Before cross-reference navigation – Run REFRESH CATALOG FULL to enable CALLERS/CALLEES/IMPACT
  • After making changes – Refresh to update the catalog with new elements
  • If results seem stale – Use REFRESH CATALOG FULL FORCE to force a rebuild

Available Tables

The catalog provides several tables that can be queried using standard SQL syntax. Use SHOW CATALOG TABLES to list all available tables in your catalog.

Discovering Tables

SHOW CATALOG TABLES;

Core Tables

CATALOG.ENTITIES

Information about all entities in the project.

ColumnDescription
IdUnique identifier
NameEntity name
ModuleNameModule containing the entity
QualifiedNameFull qualified name (Module.Entity)
EntityTypePersistent, Non-Persistent, View, External
DescriptionDocumentation text
AttributeCountNumber of attributes
AccessRuleCountNumber of access rules defined
SELECT Name, EntityType, AttributeCount
FROM CATALOG.ENTITIES
WHERE ModuleName = 'Sales'
ORDER BY Name;

CATALOG.ATTRIBUTES

Information about entity attributes.

ColumnDescription
IdUnique identifier
NameAttribute name
EntityIdParent entity ID
AttributeTypeString, Integer, Decimal, Boolean, DateTime, etc.
SELECT a.Name, a.AttributeType
FROM CATALOG.ATTRIBUTES a
JOIN CATALOG.ENTITIES e ON a.EntityId = e.Id
WHERE e.QualifiedName = 'Sales.Customer';

CATALOG.ASSOCIATIONS

Information about entity associations.

ColumnDescription
IdUnique identifier
NameAssociation name
ParentEntityParent (FROM) entity qualified name
ChildEntityChild (TO) entity qualified name
AssociationTypeReference or ReferenceSet
SELECT Name, ParentEntity, ChildEntity, AssociationType
FROM CATALOG.ASSOCIATIONS
WHERE ParentEntity LIKE '%Order%' OR ChildEntity LIKE '%Order%';

CATALOG.MICROFLOWS

Information about microflows and nanoflows.

ColumnDescription
IdUnique identifier
NameMicroflow name
ModuleNameModule containing the microflow
QualifiedNameFull qualified name
ReturnTypeReturn type of the microflow
DescriptionDocumentation text
ParametersParameter information
ObjectUsageEntities used in the microflow
SELECT Name, ReturnType, Description
FROM CATALOG.MICROFLOWS
WHERE ModuleName = 'Sales'
ORDER BY Name;

CATALOG.PAGES

Information about pages and their properties.

ColumnDescription
IdUnique identifier
NamePage name
ModuleNameModule containing the page
QualifiedNameFull qualified name
URLPage URL if configured
DataSourcePrimary data source
WidgetTypesTypes of widgets used
SELECT Name, URL, DataSource
FROM CATALOG.PAGES
WHERE ModuleName = 'Sales'
ORDER BY Name;

CATALOG.ACCESS_RULES

Information about entity access rules (available after full refresh).

ColumnDescription
IdUnique identifier
EntityIdEntity this rule applies to
UserRoleRole this rule grants access to
AllowReadWhether read access is granted
AllowWriteWhether write access is granted
SELECT e.QualifiedName, ar.UserRole, ar.AllowRead, ar.AllowWrite
FROM CATALOG.ACCESS_RULES ar
JOIN CATALOG.ENTITIES e ON ar.EntityId = e.Id
WHERE e.ModuleName = 'Sales';

Listing All Tables

To see the complete list of available tables in your catalog (which may vary by project and refresh level):

SHOW CATALOG TABLES;
mxcli -p app.mpr -c "SHOW CATALOG TABLES"

SQL Queries

The catalog supports standard SQL SELECT syntax for querying project metadata. Queries use the CATALOG. prefix to identify catalog tables.

Syntax

SELECT <columns> FROM CATALOG.<table> [WHERE <condition>] [ORDER BY <column>] [LIMIT <n>]

Standard SQL features are supported: SELECT, WHERE, JOIN, GROUP BY, ORDER BY, HAVING, UNION, LIMIT, aggregate functions (COUNT, SUM, AVG, MIN, MAX), and subqueries.

Basic Queries

Listing Elements

-- All entities in a module
SELECT Name, EntityType, Description
FROM CATALOG.ENTITIES
WHERE ModuleName = 'Sales';

-- All microflows in a module
SELECT Name, ReturnType
FROM CATALOG.MICROFLOWS
WHERE ModuleName = 'Sales';

-- Pages with URLs
SELECT Name, ModuleName, URL
FROM CATALOG.PAGES
WHERE URL IS NOT NULL;

Filtering with LIKE

-- Find entities by name pattern
SELECT QualifiedName FROM CATALOG.ENTITIES
WHERE Name LIKE '%Customer%';

-- Find microflows referencing an entity
SELECT Name, ModuleName
FROM CATALOG.MICROFLOWS
WHERE Description LIKE '%Customer%'
   OR Name LIKE '%Customer%';

-- Find pages by URL pattern
SELECT Name, ModuleName, URL
FROM CATALOG.PAGES
WHERE URL LIKE '/admin%';

Joining Tables

-- Attributes of a specific entity
SELECT a.Name, a.AttributeType
FROM CATALOG.ATTRIBUTES a
JOIN CATALOG.ENTITIES e ON a.EntityId = e.Id
WHERE e.QualifiedName = 'Sales.Customer';

-- Entity relationships
SELECT Name, ParentEntity, ChildEntity, AssociationType
FROM CATALOG.ASSOCIATIONS
WHERE ParentEntity LIKE '%Order%'
   OR ChildEntity LIKE '%Order%';

Aggregation

-- Count entities per module
SELECT ModuleName, COUNT(*) AS EntityCount
FROM CATALOG.ENTITIES
GROUP BY ModuleName
ORDER BY EntityCount DESC;

-- Documentation coverage by module
SELECT
    ModuleName,
    COUNT(*) AS TotalEntities,
    SUM(CASE WHEN Description != '' THEN 1 ELSE 0 END) AS Documented,
    ROUND(100.0 * SUM(CASE WHEN Description != '' THEN 1 ELSE 0 END) / COUNT(*), 1) AS CoveragePercent
FROM CATALOG.ENTITIES
GROUP BY ModuleName
ORDER BY CoveragePercent ASC;

Subqueries and UNION

-- Complete data flow for an entity
SELECT 'Page' AS Source, p.QualifiedName AS Element, 'displays' AS Action
FROM CATALOG.PAGES p
WHERE p.DataSource LIKE '%Customer%'
UNION ALL
SELECT 'Microflow' AS Source, m.QualifiedName AS Element, 'processes' AS Action
FROM CATALOG.MICROFLOWS m
WHERE m.ObjectUsage LIKE '%Customer%';

CLI Usage

# Run a catalog query
mxcli -p app.mpr -c "SELECT Name FROM CATALOG.ENTITIES WHERE ModuleName = 'Sales'"

# Query with limit
mxcli -p app.mpr -c "SELECT Name FROM CATALOG.MICROFLOWS LIMIT 10"

Notes

  • Always run REFRESH CATALOG (or REFRESH CATALOG FULL) before querying
  • Table and column names are case-sensitive in some contexts
  • String comparisons use SQLite semantics (case-sensitive by default with =, use LIKE for case-insensitive matching)
  • The catalog is read-only; INSERT, UPDATE, and DELETE are not supported

Use Cases

This page provides practical catalog query examples for common analysis tasks: impact analysis, finding unused elements, detecting anti-patterns, and computing complexity metrics.

Duplicate Entity Detection

Find entities with similar names across modules that may be candidates for consolidation:

SELECT
    e1.ModuleName AS Module1,
    e1.Name AS Entity1,
    e2.ModuleName AS Module2,
    e2.Name AS Entity2
FROM CATALOG.ENTITIES e1
JOIN CATALOG.ENTITIES e2
    ON LOWER(e1.Name) = LOWER(e2.Name)
    AND e1.ModuleName < e2.ModuleName
ORDER BY e1.Name;

Detect copy-paste inheritance anti-patterns by finding entities with identical attribute patterns:

SELECT
    e1.QualifiedName AS Entity1,
    e2.QualifiedName AS Entity2,
    COUNT(*) AS MatchingAttributes
FROM CATALOG.ATTRIBUTES a1
JOIN CATALOG.ATTRIBUTES a2
    ON a1.Name = a2.Name
    AND a1.AttributeType = a2.AttributeType
    AND a1.EntityId != a2.EntityId
JOIN CATALOG.ENTITIES e1 ON a1.EntityId = e1.Id
JOIN CATALOG.ENTITIES e2 ON a2.EntityId = e2.Id
WHERE e1.QualifiedName < e2.QualifiedName
GROUP BY e1.QualifiedName, e2.QualifiedName
HAVING COUNT(*) >= 3
ORDER BY MatchingAttributes DESC;

Data Lineage Analysis

Entity Dependency Graph

Find all entities connected to a given entity via associations:

SELECT
    'Outgoing' AS Direction,
    a.Name AS Association,
    a.ChildEntity AS RelatedEntity,
    a.AssociationType AS Type
FROM CATALOG.ASSOCIATIONS a
WHERE a.ParentEntity = 'Sales.Order'
UNION ALL
SELECT
    'Incoming' AS Direction,
    a.Name AS Association,
    a.ParentEntity AS RelatedEntity,
    a.AssociationType AS Type
FROM CATALOG.ASSOCIATIONS a
WHERE a.ChildEntity = 'Sales.Order';

Microflows That Modify an Entity

Track all business logic touching specific data:

SELECT
    m.QualifiedName AS Microflow,
    m.ReturnType,
    m.Description
FROM CATALOG.MICROFLOWS m
WHERE m.ObjectUsage LIKE '%Sales.Customer%'
   OR m.Parameters LIKE '%Sales.Customer%'
   OR m.ReturnType LIKE '%Sales.Customer%';

Page-to-Entity Mapping

Which pages display or edit which entities:

SELECT
    p.QualifiedName AS Page,
    p.DataSource AS Entity,
    p.WidgetTypes
FROM CATALOG.PAGES p
WHERE p.DataSource IS NOT NULL
ORDER BY p.DataSource, p.QualifiedName;

Orphan Detection

Find persistent entities with no associations, which may indicate missing relationships:

SELECT e.QualifiedName, e.EntityType, e.AttributeCount
FROM CATALOG.ENTITIES e
LEFT JOIN CATALOG.ASSOCIATIONS a
    ON e.QualifiedName = a.ParentEntity
    OR e.QualifiedName = a.ChildEntity
WHERE a.Id IS NULL
  AND e.EntityType = 'Persistent'
ORDER BY e.ModuleName, e.Name;

Complexity Analysis

Find entities with high complexity (many attributes and associations):

SELECT
    e.QualifiedName,
    e.AttributeCount,
    COUNT(a.Id) AS AssociationCount,
    e.AttributeCount + COUNT(a.Id) AS ComplexityScore
FROM CATALOG.ENTITIES e
LEFT JOIN CATALOG.ASSOCIATIONS a
    ON e.QualifiedName = a.ParentEntity
    OR e.QualifiedName = a.ChildEntity
GROUP BY e.QualifiedName, e.AttributeCount
HAVING ComplexityScore > 15
ORDER BY ComplexityScore DESC;

Documentation Coverage

Identify technical debt in documentation:

-- Entities without documentation
SELECT QualifiedName, EntityType, AttributeCount
FROM CATALOG.ENTITIES
WHERE (Description IS NULL OR Description = '')
ORDER BY AttributeCount DESC;

-- Documentation coverage by module
SELECT
    ModuleName,
    COUNT(*) AS TotalEntities,
    SUM(CASE WHEN Description != '' THEN 1 ELSE 0 END) AS Documented,
    ROUND(100.0 * SUM(CASE WHEN Description != '' THEN 1 ELSE 0 END) / COUNT(*), 1) AS CoveragePercent
FROM CATALOG.ENTITIES
GROUP BY ModuleName
ORDER BY CoveragePercent ASC;

Security Analysis

Entities Without Access Rules

SELECT
    e.QualifiedName,
    e.EntityType,
    e.AccessRuleCount
FROM CATALOG.ENTITIES e
WHERE e.AccessRuleCount = 0
  AND e.EntityType = 'Persistent'
ORDER BY e.ModuleName;

Wide-Open Entities

Review entities accessible to anonymous users:

SELECT
    e.QualifiedName,
    e.AttributeCount,
    ar.UserRole,
    ar.AllowRead,
    ar.AllowWrite
FROM CATALOG.ENTITIES e
JOIN CATALOG.ACCESS_RULES ar ON e.Id = ar.EntityId
WHERE ar.AllowRead = 1
  AND ar.UserRole = 'Anonymous'
ORDER BY e.AttributeCount DESC;

Module Health Dashboard

Overview of module complexity and documentation:

SELECT
    ModuleName,
    COUNT(DISTINCT e.Id) AS Entities,
    COUNT(DISTINCT m.Id) AS Microflows,
    COUNT(DISTINCT p.Id) AS Pages,
    ROUND(100.0 * SUM(CASE WHEN e.Description != '' THEN 1 ELSE 0 END) /
        NULLIF(COUNT(DISTINCT e.Id), 0), 1) AS DocCoverage
FROM CATALOG.ENTITIES e
LEFT JOIN CATALOG.MICROFLOWS m ON e.ModuleName = m.ModuleName
LEFT JOIN CATALOG.PAGES p ON e.ModuleName = p.ModuleName
GROUP BY ModuleName
ORDER BY Entities DESC;

Naming Convention Violations

Find entities not following PascalCase:

SELECT QualifiedName, Name
FROM CATALOG.ENTITIES
WHERE Name != UPPER(SUBSTR(Name, 1, 1)) || SUBSTR(Name, 2)
   OR Name LIKE '%_%'
   OR Name LIKE '% %';

Linting and Reports

mxcli includes an extensible linting framework that checks Mendix projects for best practice violations, security issues, naming conventions, and architectural anti-patterns. The framework combines built-in Go rules with extensible Starlark rules.

Overview

The linting system provides:

  • 14 built-in Go rules – Fast, compiled rules for common issues
  • 27 bundled Starlark rules – Extensible rules covering security, quality, architecture, design, and conventions
  • Custom rule support – Write your own rules in Starlark
  • Multiple output formats – Text, JSON, and SARIF for CI integration
  • Scored reports – Best practices report with category breakdowns

Rule Categories

CategoryPrefixFocus
MDLMDL001-MDL007Naming conventions, empty microflows, domain model size
SecuritySEC001-SEC009Access rules, password policy, demo users, PII exposure
ConventionCONV001-CONV017Best practice conventions, error handling
QualityQUAL001-QUAL004Complexity, documentation, long microflows
ArchitectureARCH001-ARCH003Cross-module data, entity business keys
DesignDESIGN001Entity attribute count

Quick Start

# Lint a project
mxcli lint -p app.mpr

# List available rules
mxcli lint -p app.mpr --list-rules

# JSON output for CI
mxcli lint -p app.mpr --format json

# Generate a scored report
mxcli report -p app.mpr

Built-in Rules

mxcli ships with 14 built-in lint rules implemented in Go. These rules are fast and always available.

MDL Rules

RuleDescription
MDL001Naming conventions – Checks entity, attribute, and microflow naming patterns
MDL002Empty microflows – Detects microflows with no activities
MDL003Domain model size – Warns when a module has too many entities
MDL004Validation feedback – Checks for proper validation feedback usage
MDL005Image source – Validates image widget source configuration
MDL006Empty containers – Detects container widgets with no children
MDL007Page navigation security – Checks that pages called from microflows have appropriate access rules

Security Rules

RuleDescription
SEC001Entity access rules – Ensures persistent entities have access rules defined
SEC002Password policy – Checks for secure password configuration
SEC003Demo users – Warns about demo users in production security level

Convention Rules

RuleDescription
CONV011No commit in loop – Detects COMMIT statements inside LOOP blocks (performance anti-pattern)
CONV012Exclusive split captions – Checks that decision branches have meaningful captions
CONV013Error handling on external calls – Ensures external service calls have error handling
CONV014No continue error handling – Warns against using CONTINUE error handling without logging

Running Built-in Rules

Built-in rules run automatically with mxcli lint:

# Run all rules (built-in + Starlark)
mxcli lint -p app.mpr

# List all rules to see which are built-in
mxcli lint -p app.mpr --list-rules

Excluding Modules

System and marketplace modules often trigger false positives. Exclude them:

mxcli lint -p app.mpr --exclude System --exclude Administration

Starlark Rules

In addition to the built-in Go rules, mxcli bundles 27 Starlark-based lint rules. Starlark is a Python-like language that allows rules to be extended and customized without recompiling mxcli.

Bundled Starlark Rules

Security Rules (SEC004-SEC009)

RuleDescription
SEC004Guest access – Warns about overly permissive guest/anonymous access
SEC005Strict mode – Checks for strict security mode settings
SEC006PII exposure – Detects potentially sensitive data without restricted access
SEC007Anonymous access – Flags entities accessible to anonymous users
SEC008Member restrictions – Checks for overly broad member-level access
SEC009Additional security rules

Architecture Rules (ARCH001-ARCH003)

RuleDescription
ARCH001Cross-module data – Detects tight coupling between modules via direct data access
ARCH002Microflow-based writes – Ensures data modifications go through microflows
ARCH003Entity business keys – Checks that entities have meaningful business key attributes

Quality Rules (QUAL001-QUAL004)

RuleDescription
QUAL001McCabe complexity – Flags microflows with high cyclomatic complexity
QUAL002Documentation – Checks for missing documentation on public elements
QUAL003Long microflows – Warns about microflows with too many activities
QUAL004Orphaned elements – Detects unused entities, microflows, or pages

Design Rules (DESIGN001)

RuleDescription
DESIGN001Entity attribute count – Warns when entities have too many attributes

Convention Rules (CONV001-CONV010, CONV015-CONV017)

RuleDescription
CONV001-CONV010Best practice conventions including boolean naming, page suffixes, enumeration prefixes, snippet prefixes
CONV015Validation rules – Checks for consistent validation patterns
CONV016Event handlers – Validates event handler configuration
CONV017Calculated attributes – Checks calculated attribute patterns

Additional convention rules cover access rule constraints, role mapping, microflow size and content.

Where Starlark Rules Live

When you run mxcli init, Starlark rules are installed to:

your-project/
└── .claude/
    └── lint-rules/
        ├── sec004_guest_access.star
        ├── arch001_cross_module.star
        ├── qual001_complexity.star
        └── ...

Running Starlark Rules

Starlark rules run automatically alongside built-in rules:

mxcli lint -p app.mpr

Use --list-rules to see all available rules:

mxcli lint -p app.mpr --list-rules

Writing Custom Rules

You can extend the linting framework by writing custom rules in Starlark, a Python-like language. Custom rules are placed in the .claude/lint-rules/ directory and are automatically picked up by mxcli lint.

Rule File Structure

Each Starlark rule file defines a rule with metadata and a check function. Place your rule files in .claude/lint-rules/ with a .star extension.

Getting Started

  1. Create a new .star file in .claude/lint-rules/
  2. Define the rule metadata (ID, name, description, severity)
  3. Implement the check function that inspects project elements
  4. Run mxcli lint -p app.mpr to test your rule

Example: Custom Naming Rule

# .claude/lint-rules/custom001_entity_prefix.star

rule_id = "CUSTOM001"
rule_name = "Entity Prefix Convention"
rule_description = "All entities should be prefixed with their module abbreviation"
severity = "warning"

def check(context):
    findings = []
    for entity in context.entities:
        # Check if entity name starts with module prefix
        module_prefix = entity.module_name[:3]
        if not entity.name.startswith(module_prefix):
            findings.append({
                "message": "Entity '%s' should start with module prefix '%s'" % (entity.name, module_prefix),
                "element": entity.qualified_name,
            })
    return findings

Rule Severity Levels

SeverityDescription
errorMust be fixed; blocks CI pipelines
warningShould be fixed; potential issue
infoInformational; suggestion for improvement

Testing Custom Rules

# Run lint to test your custom rule
mxcli lint -p app.mpr

# List rules to verify your rule is detected
mxcli lint -p app.mpr --list-rules

Best Practices

  • Use a unique rule ID prefix (e.g., CUSTOM001) to avoid conflicts with built-in rules
  • Include clear, actionable messages that explain what to fix
  • Test rules against a real project before deploying
  • Keep rules focused on a single concern

mxcli lint

The mxcli lint command runs all lint rules (built-in Go rules and Starlark rules) against a Mendix project and reports findings.

Basic Usage

# Lint a project
mxcli lint -p app.mpr

# With colored terminal output
mxcli lint -p app.mpr --color

Output Formats

Text (Default)

Human-readable output with rule IDs, severity, and messages:

mxcli lint -p app.mpr

JSON

Machine-readable JSON output for CI integration:

mxcli lint -p app.mpr --format json

SARIF

SARIF (Static Analysis Results Interchange Format) output for GitHub Code Scanning and other SARIF-compatible tools:

mxcli lint -p app.mpr --format sarif > results.sarif

This file can be uploaded to GitHub as a code scanning result.

Options

List Available Rules

mxcli lint -p app.mpr --list-rules

Shows all available rules with their IDs, names, severity, and source (built-in or Starlark).

Exclude Modules

System and marketplace modules often trigger false positives. Exclude them:

mxcli lint -p app.mpr --exclude System --exclude Administration

Colored Output

mxcli lint -p app.mpr --color

CI Integration

GitHub Actions

- name: Lint Mendix project
  run: |
    mxcli lint -p app.mpr --format sarif > results.sarif

- name: Upload SARIF
  uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: results.sarif

JSON Processing

# Count findings by severity
mxcli lint -p app.mpr --format json | jq 'group_by(.severity) | map({severity: .[0].severity, count: length})'

# Filter for errors only
mxcli lint -p app.mpr --format json | jq '[.[] | select(.severity == "error")]'

Exit Codes

CodeMeaning
0No findings
1Findings reported
2Error running linter

mxcli report

The mxcli report command generates a scored best practices report for a Mendix project. It runs all lint rules and aggregates findings into a category-based scorecard.

Basic Usage

# Generate a report (text output to terminal)
mxcli report -p app.mpr

Output Formats

Markdown

mxcli report -p app.mpr --format markdown
mxcli report -p app.mpr --format markdown --output report.md

HTML

Visual report with color-coded scoring:

mxcli report -p app.mpr --format html --output report.html

JSON

Machine-readable report for CI pipelines:

mxcli report -p app.mpr --format json

Scoring Categories

The report scores the project across 6 categories on a 0-100 scale:

CategoryWhat It Measures
SecurityAccess rules, password policy, demo users, PII exposure
QualityDocumentation coverage, complexity, orphaned elements
ArchitectureModule coupling, data access patterns, business keys
PerformanceCommit-in-loop, query patterns
NamingEntity, attribute, microflow, and page naming conventions
DesignEntity size, attribute counts, association patterns

Each category shows:

  • A score from 0 to 100
  • Number of findings in that category
  • Specific rule violations with affected elements

Writing Reports to Files

# Write HTML report
mxcli report -p app.mpr --format html --output report.html

# Write Markdown report
mxcli report -p app.mpr --format markdown --output report.md

# Write JSON report
mxcli report -p app.mpr --format json --output report.json

CI Integration

Use the JSON format to fail CI pipelines when scores drop below a threshold:

# Check if overall score is above 70
SCORE=$(mxcli report -p app.mpr --format json | jq '.overallScore')
if [ "$SCORE" -lt 70 ]; then
  echo "Quality score $SCORE is below threshold 70"
  exit 1
fi

Terminal UI (TUI)

mxcli includes a terminal-based interactive UI for browsing, inspecting, and modifying Mendix projects. Built with Bubble Tea, it provides a ranger/yazi-style Miller column interface directly in the terminal.

Quick Start

# Launch with a project file
mxcli tui -p app.mpr

# Resume a previous session
mxcli tui -c

# Launch without a project (opens file picker)
mxcli tui

The TUI opens in full-screen alternate screen mode with mouse support enabled.

Feature Summary

FeatureDescription
Miller column navigationThree-panel layout: modules, documents, preview
Vim-style keybindingsh/j/k/l navigation, / to filter, : for commands
Syntax highlightingReal-time MDL, SQL, and NDSL highlighting via Chroma
Command paletteVS Code-style : command bar with tab completion
MDL executionType or paste MDL scripts and execute them in-place
mx check integrationValidate projects with grouped errors, navigation, and filtering
Mouse supportClick to select, scroll wheel, clickable breadcrumbs
Session restore-c flag restores previous tabs, selections, and navigation state
File watcherAuto-detects MPR changes and refreshes the tree
Fullscreen previewPress Enter or Z to expand any panel to full screen
Tab supportMultiple project tabs in a single session
Contextual helpPress ? for keybinding reference

Architecture

The TUI delegates all heavy lifting to existing mxcli logic. The project tree is built by calling buildProjectTree() directly, while commands like describe, callers, check, and exec run mxcli as a subprocess. Diagrams open in the system browser via xdg-open or open.

Flags

FlagDescription
-p, --projectPath to the .mpr project file
-c, --continueRestore the previous TUI session (tabs, navigation, preview mode)

When launched without -p, the TUI presents a file picker that shows recently opened projects and allows browsing the filesystem for .mpr files.

Keyboard Reference

KeyAction
h / LeftMove focus left
l / Right / EnterMove focus right / open
j / DownMove down
k / UpMove up
TabCycle panel focus
/Filter current column
:Open command palette
xOpen MDL execution view
z / ZToggle fullscreen zoom on current panel
?Show contextual help
qQuit

Navigation and Layout

The TUI uses a Miller column layout for hierarchical browsing of Mendix projects. This design, inspired by file managers like ranger and yazi, shows the current context alongside parent and child items for spatial orientation.

Three-Column Layout

The default layout divides the terminal into three panels:

PanelContentWidth
LeftModules and folders~20%
CenterDocuments within the selected module (entities, microflows, pages, etc.)~30%
RightPreview of the selected document (MDL/NDSL rendering with syntax highlighting)~50%

The layout adapts dynamically as you navigate:

  • One panel: Only the module list is visible when no module is selected.
  • Two panels: Selecting a module expands the document list alongside it (35% + 65% split).
  • Three panels: Selecting a document adds the preview panel (20% + 30% + 50% split).

Each panel displays a breadcrumb trail at the top showing the current navigation path. For example:

MyModule > DomainModel > Customer

Breadcrumbs are clickable with the mouse. Clicking a segment navigates back to that level in the hierarchy. The navigation stack remembers your path so you can drill into nested folders and return.

Keyboard Navigation

The TUI uses vim-style keybindings for movement:

KeyAction
j / DownMove cursor down in the current panel
k / UpMove cursor up in the current panel
h / LeftMove focus to the left panel (or go back in navigation stack)
l / Right / EnterMove focus to the right panel (or drill into the selected item)
TabCycle focus between panels (left, center, right)
/Activate filter mode in the current panel
EscExit filter mode, close overlay, or cancel current action

Filtering

Press / to activate the filter input in the current panel. Type a substring to filter the list in real time. Only items matching the filter text are shown. Press Esc to clear the filter and return to the full list.

Mouse Support

The TUI enables mouse interaction for all panels:

ActionEffect
Click on an itemSelects it and focuses the panel
Scroll wheelScrolls the list up or down
Click on a breadcrumb segmentNavigates back to that level

Mouse coordinates are translated to panel-local positions using the layout geometry, so clicks always target the correct panel regardless of terminal size.

Fullscreen Overlay

Press Enter or Z on a selected document to open a fullscreen overlay showing the complete document content. The overlay includes:

  • A title bar with the document name and type
  • Scrollable content with syntax highlighting (MDL, SQL, or NDSL)
  • Hint bar at the bottom showing available keys

Press Esc or q to close the overlay and return to the three-column browser.

Zoom Mode

Press z to zoom the currently focused panel to fill the entire terminal. This is useful for reviewing long module lists or document trees. Press z again or Esc to return to the previous layout. The TUI remembers which panel was focused and which layout was active before zooming.

Tab Support

The TUI supports multiple project tabs within a single session. Each tab maintains its own:

  • Project file path
  • Navigation state (selected module, document, preview)
  • Panel focus and scroll positions

Tabs allow you to compare or cross-reference elements across different projects or different areas of the same project.

Contextual Help

Press ? at any time to open the help overlay. The help content is contextual – it shows keybindings relevant to the current mode (browser, overlay, command palette, or execution view).

Command Palette

Press : to open the command palette at the bottom of the TUI. The command bar provides VS Code-style command execution with tab completion and fuzzy matching.

Usage

  1. Press : to activate the command bar.
  2. Start typing a command name. Tab completion suggests matching commands.
  3. Press Enter to execute or Esc to cancel.

Available Commands

CommandDescription
:checkValidate the project with mx check
:runExecute the current MDL file
:callersShow callers of the selected element
:calleesShow callees of the selected element
:contextShow context of the selected element
:impactShow impact analysis of the selected element
:refsShow references to the selected element
:diagramOpen a diagram of the selected element in the system browser
:search <keyword>Perform full-text search across the project

Tab Completion

The command bar supports multi-level tab completion:

  1. Level 1 – Command name: Type the first few characters and press Tab to complete. For example, cal + Tab completes to callers.
  2. Level 2 – Subcommand: Some commands have subcommands. Tab cycles through available options.
  3. Level 3 – Qualified name: Commands that operate on elements (callers, callees, refs, impact, context) complete with qualified names from the project tree. For example, :callers MyMod + Tab expands to :callers MyModule.ACT_CreateCustomer.

Fuzzy matching allows partial input. Typing :cal matches :callers and :callees.

Code Navigation Commands

The :callers, :callees, :refs, :impact, and :context commands display their results in a fullscreen overlay with syntax highlighting. These commands require a prior REFRESH CATALOG FULL to populate the cross-reference index.

When the currently selected element in the browser is a microflow, page, or other navigable document, these commands automatically use it as the target. You can also provide an explicit qualified name:

:callers MyModule.ACT_CreateCustomer
:impact MyModule.Customer

The :search command takes a keyword argument and performs full-text search across all project strings:

:search validation
:search Customer

Results appear in a fullscreen overlay listing matching elements with their type and location.

Diagram

The :diagram command opens a visual diagram of the selected element in the system browser. It uses xdg-open on Linux or open on macOS to launch the default browser. This is useful for viewing domain model relationships or microflow graphs in a visual format.

MDL Execution

The TUI includes a built-in MDL execution view for running MDL statements directly from the terminal interface. Press x to open the execution view.

Opening the Execution View

From the browser mode, press x to switch to the MDL execution view. A full-screen textarea appears with line numbers and a placeholder prompt.

Keybindings

KeyAction
xOpen the execution view (from browser mode)
Ctrl+EExecute the MDL script in the textarea
Ctrl+OOpen a file picker to load an MDL file
EscClose the execution view and return to the browser

Writing and Executing MDL

The textarea supports standard text editing. Type or paste any MDL statements:

SHOW MODULES;

CREATE ENTITY MyModule.Customer
  NAME "Customer"
  PERSISTENT
  ATTRIBUTE Name STRING(100)
  ATTRIBUTE Email STRING(200)
  ATTRIBUTE Age INTEGER;

Press Ctrl+E to execute. The TUI writes the content to a temporary file and runs mxcli exec as a subprocess. During execution, a status indicator shows “Executing…” in the status bar.

Viewing Results

After execution completes, results appear in a fullscreen overlay with syntax highlighting. The overlay shows:

  • The command output for successful executions (e.g., confirmation messages, query results)
  • Error details for failed executions, prefixed with -- Error:

Press q or Esc to close the result overlay.

Tree Refresh

When an MDL script executes successfully (e.g., creating or modifying elements), the project tree automatically refreshes to reflect the changes. This means you can create an entity, close the result overlay, and immediately see it in the browser.

Loading from File

Press Ctrl+O to open a file picker dialog. The TUI attempts to use a native file picker:

  • Linux: zenity --file-selection or kdialog --getopenfilename
  • macOS: Native file dialog

The file picker filters for .mdl files. After selecting a file, its content is loaded into the textarea. A status message confirms the loaded file path. You can then review or edit the content before executing with Ctrl+E.

If no file picker is available, the TUI displays an informational message suggesting installation of zenity or kdialog.

Supported MDL Statements

The execution view accepts any valid MDL statement, including:

CategoryExamples
QuerySHOW MODULES, SHOW ENTITIES, DESCRIBE ENTITY MyModule.Customer
CreateCREATE ENTITY, CREATE MICROFLOW, CREATE PAGE
ModifyALTER ENTITY, ALTER PAGE, ALTER NAVIGATION
DeleteDROP ENTITY, DROP MICROFLOW, DROP PAGE
SecurityGRANT, REVOKE, CREATE USER ROLE
CatalogREFRESH CATALOG, SELECT FROM CATALOG.ENTITIES
SearchSEARCH 'keyword'

Multiple statements can be included in a single execution, separated by semicolons.

Project Validation

The TUI integrates with mx check to validate Mendix projects directly from the terminal interface. Errors, warnings, and deprecations are displayed in an interactive overlay with grouping, navigation, and filtering.

Running a Check

Use the :check command from the command palette (press : then type check) to validate the current project. The TUI runs mx check -j -w -d in the background and displays results in a fullscreen overlay.

Error Overlay

The check results overlay groups diagnostics by error code and deduplicates repeated occurrences:

mx check Results  [All: 8E 2W 1D]

CE1613 -- The selected association/attribute no longer exists
  MyFirstModule.P_ComboBox_Enum (Page)
    > Property 'Association' of combo box 'cmbPriority'
  MyFirstModule.P_ComboBox_Assoc (Page) (x7)
    > Property 'Attribute' of combo box 'cmbCategory'

Each group shows:

  • The error code and description
  • Affected documents with their type
  • Specific element details
  • Occurrence count for deduplicated entries (shown as (xN))

Filtering by Severity

The overlay supports filtering by diagnostic severity. Press Tab to cycle through filters:

FilterShows
AllErrors, warnings, and deprecations
ErrorsOnly errors
WarningsOnly warnings
DeprecationsOnly deprecations

The title bar reflects the active filter: [All: 8E 2W 1D] or [Errors: 8].

Error Navigation

The check overlay is a selectable list. Use j/k to move the cursor between error locations. Press Enter on a location to:

  1. Close the overlay
  2. Navigate the project tree to the affected document

After closing the overlay, the TUI enters check navigation mode. The status bar shows the current error position and navigation hints:

[2/5] CE1613: MyFirstModule.P_ComboBox_Enum  ]e next  [e prev
KeyAction
]eJump to the next error location in the project tree
[eJump to the previous error location
!Reopen the check overlay
EscExit check navigation mode

File Monitoring

The TUI watches the project file for changes. When it detects that the MPR file has been modified (e.g., after saving in Studio Pro or executing an MDL script), it can automatically re-run mx check and update the results. This provides a continuous validation feedback loop when making changes.

LLM Anchor Tags

The check overlay embeds faint structured anchor tags in its output for machine consumption. These tags are nearly invisible in the terminal but are preserved when copying text or taking screenshots:

[mxcli:check] errors=8 warnings=2 deprecations=1
[mxcli:check:CE1613] severity=ERROR count=6 doc=MyFirstModule.P_ComboBox_Assoc type=Page

These anchors enable AI assistants to parse check results from terminal screenshots or clipboard content, providing structured data for automated error analysis and fix suggestions.

Status Badge

When check results are available, a compact badge appears in the TUI status bar showing the diagnostic summary:

8E 2W 1D

This badge updates whenever a new check completes, providing an at-a-glance project health indicator without opening the full overlay.

Session Management

The TUI supports session persistence, allowing you to restore your previous browsing state when relaunching the tool.

Restoring a Session

Use the -c or --continue flag to restore the previous session:

mxcli tui -c

This restores:

  • Open project tabs and their file paths
  • Navigation state (selected module, document, scroll positions)
  • Panel focus and layout configuration
  • Preview mode settings

If the session file cannot be loaded (e.g., first launch or corrupted state), a warning is printed to stderr and the TUI starts fresh.

Combining with Project Flag

When using -c with -p, the explicit project path takes precedence:

# Restore session but open a specific project
mxcli tui -c -p app.mpr

When using -c alone, the project path is loaded from the saved session. If the session contains multiple tabs, the first tab’s project path is used.

Session Storage

Session state is stored locally in the user’s configuration directory. The TUI automatically saves session state when exiting normally (via q).

Project File Picker

When launching without either -p or -c, the TUI presents an interactive file picker:

mxcli tui

The picker shows:

  • Recently opened projects (from session history)
  • Filesystem browser filtered to .mpr files

Select a project and press Enter to launch the TUI. The selected project is saved to the history for future sessions.

Session History

The TUI maintains a history of opened projects. This history is separate from the full session state and persists across sessions. It powers:

  • The file picker’s “recent projects” list
  • Session restore when using -c

Each time a project is opened, it is added to the history via tui.SaveHistory(projectPath).

Testing

mxcli includes a testing framework for validating Mendix projects using MDL test files. Tests verify that MDL scripts execute correctly and that the resulting project passes validation.

Overview

The testing framework supports two test file formats:

FormatExtensionDescription
MDL test files.test.mdlPure MDL scripts with test annotations
Markdown test files.test.mdLiterate tests with prose and embedded MDL code blocks

Prerequisites

Running tests requires Docker for Mendix runtime validation. The test runner:

  1. Creates a fresh Mendix project using mx create-project
  2. Executes the MDL test script against the project
  3. Validates the result with mx check
  4. Reports pass/fail results

Quick Start

# Run all tests in a directory
mxcli test tests/ -p app.mpr

# Run a specific test file
mxcli test tests/sales.test.mdl -p app.mpr

Test Workflow

  1. Write test files using .test.mdl or .test.md format
  2. Add @test and @expect annotations for assertions
  3. Run tests with mxcli test
  4. Review results

Example Test

-- tests/customer.test.mdl

-- @test Create customer entity
CREATE PERSISTENT ENTITY MyFirstModule.Customer (
    Name: String(200) NOT NULL,
    Email: String(200)
);
-- @expect 0 errors

-- @test Add association
CREATE ASSOCIATION MyFirstModule.Order_Customer
    FROM MyFirstModule.Order
    TO MyFirstModule.Customer
    TYPE Reference;
-- @expect 0 errors

Playwright UI Testing

For browser-based testing that verifies widgets render correctly in the DOM, see Playwright Testing. While mxcli test validates that MDL scripts execute correctly and the project passes mx check, Playwright testing goes further by verifying the running application in a real browser – checking that widgets are visible, forms accept input, and navigation works.

# MDL validation (this page)
mxcli test tests/ -p app.mpr

# Browser-based UI verification
mxcli playwright verify tests/ -p app.mpr

Test Formats

mxcli supports two test file formats: .test.mdl for pure MDL tests and .test.md for literate tests with documentation.

.test.mdl Format

Pure MDL scripts with test annotations. Each test case is marked with @test and optionally @expect.

-- @test Create a new module
CREATE MODULE TestModule;
-- @expect 0 errors

-- @test Create entity with attributes
CREATE PERSISTENT ENTITY TestModule.Customer (
    Name: String(200) NOT NULL,
    Email: String(200) UNIQUE,
    IsActive: Boolean DEFAULT true
);
-- @expect 0 errors

-- @test Create enumeration
CREATE ENUMERATION TestModule.Status (
    Active 'Active',
    Inactive 'Inactive'
);
-- @expect 0 errors

-- @test Create microflow
CREATE MICROFLOW TestModule.ACT_Activate(
    $customer: TestModule.Customer
) RETURNS Boolean AS $result
BEGIN
    DECLARE $result Boolean = false;
    CHANGE $customer (IsActive = true);
    COMMIT $customer;
    SET $result = true;
    RETURN $result;
END;
-- @expect 0 errors

.test.md Format

Literate test files that combine prose documentation with embedded MDL code blocks. The test runner extracts and executes the MDL code blocks while ignoring the prose.

# Customer Module Tests

This test suite validates the customer management module.

## Entity Creation

Create the customer entity with standard fields:

```sql
CREATE PERSISTENT ENTITY TestModule.Customer (
    Name: String(200) NOT NULL,
    Email: String(200)
);
```

## Association Setup

Link customers to their orders:

```sql
CREATE ASSOCIATION TestModule.Order_Customer
    FROM TestModule.Order
    TO TestModule.Customer
    TYPE Reference;
```

The .test.md format is useful for:

  • Documenting test intent alongside the test code
  • Creating test suites that serve as tutorials
  • Sharing test cases with non-technical stakeholders

File Organization

tests/
├── domain-model.test.mdl     # Entity and association tests
├── microflows.test.mdl        # Microflow logic tests
├── security.test.mdl          # Access rule tests
├── pages.test.mdl             # Page creation tests
└── integration.test.md        # Full integration test with docs

Naming Conventions

  • Use descriptive file names that indicate what is being tested
  • Group related tests in the same file
  • Use the @test annotation to name individual test cases within a file

Test Annotations

Test annotations control test execution and define expectations. They are placed in comments within .test.mdl files.

@test

Marks the start of a named test case.

Syntax:

-- @test <description>

Example:

-- @test Create customer entity
CREATE PERSISTENT ENTITY MyModule.Customer (
    Name: String(200) NOT NULL
);

Each @test annotation starts a new test case. All MDL statements between one @test and the next (or end of file) belong to that test case.

@expect

Defines the expected outcome of a test case.

Syntax:

-- @expect <expectation>

Expected Outcomes

ExpectationDescription
0 errorsThe test should complete with no errors from mx check
errorThe test is expected to produce an error
<n> errorsThe test should produce exactly N errors

Examples:

-- @test Valid entity creation
CREATE PERSISTENT ENTITY MyModule.Customer (
    Name: String(200) NOT NULL
);
-- @expect 0 errors

-- @test Missing module should fail
CREATE PERSISTENT ENTITY NonExistent.Entity (
    Name: String(200)
);
-- @expect error

Complete Example

-- @test Setup: Create module and enumeration
CREATE MODULE TestSales;

CREATE ENUMERATION TestSales.OrderStatus (
    Draft 'Draft',
    Submitted 'Submitted',
    Completed 'Completed'
);
-- @expect 0 errors

-- @test Create entity with enum attribute
CREATE PERSISTENT ENTITY TestSales.Order (
    OrderNumber: String(50) NOT NULL,
    Status: Enumeration(TestSales.OrderStatus) DEFAULT 'Draft',
    TotalAmount: Decimal DEFAULT 0
);
-- @expect 0 errors

-- @test Create microflow with validation
CREATE MICROFLOW TestSales.ACT_SubmitOrder(
    $order: TestSales.Order
) RETURNS Boolean AS $success
BEGIN
    DECLARE $success Boolean = false;

    IF $order/TotalAmount <= 0 THEN
        VALIDATION FEEDBACK $order/TotalAmount MESSAGE 'Total must be positive';
        RETURN false;
    END IF;

    CHANGE $order (Status = 'Submitted');
    COMMIT $order;
    SET $success = true;
    RETURN $success;
END;
-- @expect 0 errors

-- @test Security: grant access
CREATE MODULE ROLE TestSales.User;
GRANT EXECUTE ON MICROFLOW TestSales.ACT_SubmitOrder TO TestSales.User;
GRANT TestSales.User ON TestSales.Order (CREATE, DELETE, READ *, WRITE *);
-- @expect 0 errors

Notes

  • Test cases run sequentially; earlier test cases set up state for later ones
  • The test runner validates using mx check after executing each test case (or the full script)
  • Tests that modify the project are run against an isolated copy, not the original MPR file

Running Tests

The mxcli test command executes test files and reports results.

Prerequisites

Running tests requires Docker for Mendix runtime validation. The test runner uses:

  • mx create-project to create a fresh blank Mendix project
  • mx check to validate the project after applying MDL changes

The mx binary is located at:

EnvironmentPath
Dev container~/.mxcli/mxbuild/{version}/modeler/mx
Repositoryreference/mxbuild/modeler/mx

To auto-download mxbuild for the project’s Mendix version:

mxcli setup mxbuild -p app.mpr

Basic Usage

# Run all tests in a directory
mxcli test tests/ -p app.mpr

# Run a specific test file
mxcli test tests/sales.test.mdl -p app.mpr

# Run markdown test files
mxcli test tests/integration.test.md -p app.mpr

Test Execution Flow

  1. Create project – A fresh Mendix project is created in a temporary directory using mx create-project
  2. Execute MDL – The test script is executed against the fresh project using mxcli exec
  3. Validatemx check validates the resulting project for errors
  4. Report – Results are reported with pass/fail status per test case

Isolated Testing Pattern

For manual testing or debugging, you can replicate the test runner’s workflow:

# Create a fresh project
cd /tmp/test-workspace
~/.mxcli/mxbuild/*/modeler/mx create-project

# Apply MDL changes
mxcli exec script.mdl -p /tmp/test-workspace/App.mpr

# Validate
~/.mxcli/mxbuild/*/modeler/mx check /tmp/test-workspace/App.mpr

Expected output for a passing test:

Checking app for errors...
The app contains: 0 errors.

Test Validation Checklist

Before marking tests as complete:

  • MDL script executes without errors
  • mx check passes with 0 errors
  • Created elements appear correctly if inspected with DESCRIBE
  • Security rules are valid (no CE0066 errors)

Debugging Test Failures

TypeCacheUnknownTypeException

The type cache does not contain a type with qualified name DomainModels$Index

This means a BSON $Type field uses the wrong name. Check the reflection data for the correct storageName.

CE0066 “Entity access is out of date”

Security access rules don’t match the entity’s current structure. Make sure:

  • All attributes in the entity are included in access rules
  • Association member access is only on the FROM entity
  • Run GRANT after adding new attributes

Common Fixes

# Re-run with verbose output
mxcli exec script.mdl -p app.mpr --verbose

# Inspect the generated BSON
mxcli dump-bson -p app.mpr --doc "Module.EntityName"

Playwright Testing

mxcli integrates with playwright-cli for automated UI testing of Mendix applications. This enables verification that generated pages render correctly, widgets appear in the DOM, and navigation works as expected.

Why Browser Testing?

Mendix apps are React single-page applications. The server returns a JavaScript shell, and the actual UI is rendered client-side. An HTTP 200 from /p/Customer_Overview tells you nothing about whether the page’s widgets actually rendered. A button defined in MDL might be missing from the DOM due to conditional visibility, incorrect container nesting, or a BSON serialization issue – none of which are detectable without executing JavaScript in a real browser.

Prerequisites

  • Node.js – Required for playwright-cli
  • playwright-cli – Install globally: npm install -g @playwright/cli@latest
  • Chromium – Install via: playwright-cli install --with-deps chromium
  • Running Mendix app – Start with mxcli docker run -p app.mpr --wait

Devcontainer Setup

If using a devcontainer, add the following to postCreateCommand in your devcontainer.json:

npm install -g @playwright/cli@latest && playwright-cli install --with-deps chromium

The mxcli init command generates a .playwright/cli.config.json with sensible defaults for headless Chromium and a PLAYWRIGHT_CLI_SESSION environment variable for session management.

mxcli playwright verify

The mxcli playwright verify command runs .test.sh scripts against a running Mendix application and collects results.

# Run all test scripts in a directory
mxcli playwright verify tests/ -p app.mpr

# Run a specific script
mxcli playwright verify tests/verify-customers.test.sh

# List discovered scripts without executing
mxcli playwright verify tests/ --list

# Output JUnit XML for CI integration
mxcli playwright verify tests/ -p app.mpr --junit results.xml

# Verbose output (show script stdout/stderr)
mxcli playwright verify tests/ -p app.mpr --verbose

# Custom app URL (auto-detected from .docker/.env by default)
mxcli playwright verify tests/ --base-url http://localhost:9090

# Skip the app health check
mxcli playwright verify tests/ --skip-health-check

Options

FlagDefaultDescription
--list, -lfalseList test scripts without executing
--junit, -jWrite JUnit XML results to file
--verbose, -vfalseShow script stdout/stderr during execution
--colorfalseUse colored terminal output
--timeout, -t2mTimeout per script execution
--base-urlhttp://localhost:8080Mendix app base URL
--skip-health-checkfalseSkip app reachability check before running
-pPath to the .mpr project file

How It Works

The verify runner performs these steps:

  1. Discovers all .test.sh files in the provided paths
  2. Checks that playwright-cli is available in PATH
  3. Health-checks the app at the base URL (unless --skip-health-check)
  4. Opens a playwright-cli browser session (Chromium by default)
  5. Runs each .test.sh script sequentially via bash
  6. Captures a screenshot on failure for debugging
  7. Closes the browser session
  8. Reports pass/fail per script with timing
  9. Writes JUnit XML if --junit is specified
  10. Exits with non-zero status if any script failed

Test Script Format

Test scripts are plain bash files using playwright-cli commands. The naming convention is tests/verify-<name>.test.sh. Scripts should use set -euo pipefail so that any failing command causes the script to exit with a non-zero code.

Example Test Script

#!/usr/bin/env bash
# tests/verify-customers.test.sh -- Customer module smoke test
set -euo pipefail

# --- Login ---
playwright-cli open http://localhost:8080
playwright-cli fill e12 "MxAdmin"
playwright-cli fill e15 "AdminPassword1!"
playwright-cli click e18
playwright-cli state-save mendix-auth

# --- Customer Overview page ---
playwright-cli goto http://localhost:8080/p/Customer_Overview
playwright-cli run-code "document.querySelector('.mx-name-dgCustomers') !== null"
playwright-cli run-code "document.querySelector('.mx-name-btnNew') !== null"
playwright-cli run-code "document.querySelector('.mx-name-btnEdit') !== null"

# --- Create a customer ---
playwright-cli click btnNew
playwright-cli snapshot
playwright-cli fill txtName "CI Test Customer"
playwright-cli fill txtEmail "ci@test.com"
playwright-cli click btnSave

# --- Verify persistence (via OQL, not direct DB query) ---
mxcli oql -p app.mpr --json \
  "SELECT Name FROM MyModule.Customer WHERE Name = 'CI Test Customer'" \
  | grep -q "CI Test Customer"

# --- Cleanup ---
playwright-cli close
echo "PASS: verify-customers"

Widget Selectors

Mendix renders each widget’s name property as a CSS class on the corresponding DOM element:

<div class="mx-name-submitButton form-group">

These .mx-name-* classes are stable, predictable, and directly derived from MDL widget names. This makes them ideal for test assertions.

Using Selectors in Scripts

The recommended approach for CI scripts is to use run-code with CSS selectors, since dynamic element refs (e12, e15) change between page loads:

# Check widget presence
playwright-cli run-code "document.querySelector('.mx-name-btnSave') !== null"

# Click a widget
playwright-cli run-code "document.querySelector('.mx-name-btnSave').click()"

# Check text content
playwright-cli run-code "document.querySelector('.mx-name-lblTitle').textContent.includes('Customers')"

For assertions that should cause failures, use throw so the script exits non-zero under set -e:

playwright-cli run-code "if (!document.querySelector('.mx-name-btnSave')) throw new Error('btnSave not found')"

Three Test Layers

Layer 1: Smoke Tests

Fast checks that pages are reachable and the app starts without errors. These run first as a gate before heavier tests.

# HTTP reachability
playwright-cli open http://localhost:8080
playwright-cli goto http://localhost:8080/p/Customer_Overview
playwright-cli goto http://localhost:8080/p/Customer_Edit

Layer 2: UI Widget Tests

Verify that every widget generated in MDL is present and interactive in the DOM.

# Widget presence
playwright-cli run-code "document.querySelector('.mx-name-dgCustomers') !== null"
playwright-cli run-code "document.querySelector('.mx-name-btnNew') !== null"

# Form interaction
playwright-cli run-code "document.querySelector('.mx-name-txtName input').value = 'Test'"
playwright-cli run-code "document.querySelector('.mx-name-btnSave').click()"

Layer 3: Data Assertions

Verify that UI interactions persist the correct data. Use mxcli oql for data validation instead of direct database queries.

# Submit a form via the UI, then verify persistence
mxcli oql -p app.mpr --json \
  "SELECT Name, Email FROM MyModule.Customer WHERE Name = 'Test'" \
  | grep -q "Test"

Example Output

Found 3 test script(s)
Checking app at http://localhost:8080...
  App is reachable
Opening browser session (chromium)...
  [1/3] verify-login... PASS (1.2s)
  [2/3] verify-customers... PASS (3.4s)
  [3/3] verify-orders... FAIL (2.1s)
         Screenshot saved: verify-orders-failure.png
         Error: btnSubmit not found

Playwright Verify Results
============================================================
  PASS  verify-login (1.2s)
  PASS  verify-customers (3.4s)
  FAIL  verify-orders (2.1s)
         Error: btnSubmit not found
------------------------------------------------------------
Total: 3  Passed: 2  Failed: 1  Time: 6.7s
Some scripts failed.

CI/CD Integration

For continuous integration, combine mxcli docker run --wait with mxcli playwright verify:

# Build and start the app
mxcli docker run -p app.mpr --wait

# Run all verification scripts
mxcli playwright verify tests/ -p app.mpr --junit results.xml

# JUnit XML output is consumed by CI systems (GitHub Actions, Jenkins, etc.)
  • Testing – MDL test framework (mxcli test)
  • Docker Run – Building and running the Mendix app in Docker
  • Docker Check – Validating projects without building

Diff

mxcli provides two diff commands for comparing MDL scripts against project state and viewing local changes in MPR v2 projects.

mxcli diff

Compares an MDL script against the current project state, showing what would change if the script were executed. This is a dry-run preview.

Usage:

mxcli diff -p app.mpr changes.mdl

This shows:

  • Elements that would be created (new entities, microflows, pages)
  • Elements that would be modified (changed attributes, altered properties)
  • Elements that would be removed (DROP statements)

Use mxcli diff to review changes before applying them, especially when working with AI-generated scripts.

mxcli diff-local

Compares local changes against a git reference for MPR v2 projects. MPR v2 (Mendix >= 10.18) stores documents as individual files in an mprcontents/ folder, making git diff feasible.

Usage:

# Compare against HEAD (latest commit)
mxcli diff-local -p app.mpr --ref HEAD

# Compare against a specific commit
mxcli diff-local -p app.mpr --ref HEAD~1

# Compare against a branch
mxcli diff-local -p app.mpr --ref main

MPR v2 Requirement

diff-local only works with MPR v2 format (Mendix >= 10.18), where documents are stored as individual files. MPR v1 projects store everything in a single SQLite database, making file-level git diff impractical.

Workflow

Review Before Applying

# 1. Generate MDL changes
# (AI assistant creates changes.mdl)

# 2. Review what would change
mxcli diff -p app.mpr changes.mdl

# 3. If satisfied, apply
mxcli exec changes.mdl -p app.mpr

Track Changes Over Time

# After making changes, see what changed since last commit
mxcli diff-local -p app.mpr --ref HEAD

# See changes since two commits ago
mxcli diff-local -p app.mpr --ref HEAD~2

External SQL

mxcli can connect to external databases (PostgreSQL, Oracle, SQL Server) for querying, schema exploration, data import, and Database Connector generation. This enables workflows like importing reference data from external systems, exploring database schemas, and auto-generating Mendix integration code.

Supported Databases

DatabaseDriver NamesDSN Format
PostgreSQLpostgres, pg, postgresqlpostgres://user:pass@host:5432/dbname
Oracleoracle, oraoracle://user:pass@host:1521/service
SQL Serversqlserver, mssqlsqlserver://user:pass@host:1433?database=dbname

Security

Credentials are isolated from session output. The DSN (which contains username and password) is never displayed in session output, logs, or error messages. Only the alias and driver name are shown.

Capabilities

FeatureCommandDescription
ConnectSQL CONNECTEstablish a named connection
ExploreSQL <alias> SHOW TABLES/VIEWSBrowse database schema
DescribeSQL <alias> DESCRIBE <table>View column details
QuerySQL <alias> <any-sql>Run arbitrary SQL
ImportIMPORT FROM <alias>Import data into Mendix app DB
GenerateSQL <alias> GENERATE CONNECTORAuto-generate Database Connector MDL
DisconnectSQL DISCONNECT <alias>Close connection

Quick Start

-- Connect to an external database
SQL CONNECT postgres 'postgres://user:pass@localhost:5432/mydb' AS source;

-- Explore the schema
SQL source SHOW TABLES;
SQL source DESCRIBE employees;

-- Query data
SQL source SELECT * FROM employees WHERE active = true LIMIT 10;

-- Import into Mendix
IMPORT FROM source QUERY 'SELECT name, email FROM employees'
  INTO HR.Employee
  MAP (name AS Name, email AS Email);

-- Generate Database Connector MDL
SQL source GENERATE CONNECTOR INTO HRModule TABLES (employees, departments) EXEC;

-- Disconnect
SQL DISCONNECT source;

CLI Subcommand

For one-off queries without the REPL:

mxcli sql --driver postgres --dsn 'postgres://user:pass@localhost:5432/mydb' "SELECT * FROM users"

SQL CONNECT

The SQL CONNECT command establishes a named connection to an external database.

Syntax

SQL CONNECT <driver> '<dsn>' AS <alias>

Parameters:

ParameterDescription
<driver>Database driver name
<dsn>Data Source Name (connection string)
<alias>Short name to reference this connection

Supported Drivers

DatabaseDriver Names
PostgreSQLpostgres, pg, postgresql
Oracleoracle, ora
SQL Serversqlserver, mssql

Connection Examples

PostgreSQL

SQL CONNECT postgres 'postgres://user:password@localhost:5432/mydb' AS source;

-- With SSL
SQL CONNECT postgres 'postgres://user:password@host:5432/mydb?sslmode=require' AS prod;

Oracle

SQL CONNECT oracle 'oracle://user:password@host:1521/service_name' AS oradb;

SQL Server

SQL CONNECT sqlserver 'sqlserver://user:password@host:1433?database=mydb' AS mssql;

Managing Connections

List Active Connections

SQL CONNECTIONS;

This shows alias and driver name only. The DSN is never displayed for security reasons.

Disconnect

SQL DISCONNECT source;

Multiple Connections

You can maintain multiple simultaneous connections with different aliases:

SQL CONNECT postgres 'postgres://...' AS source_db;
SQL CONNECT oracle 'oracle://...' AS legacy_db;
SQL CONNECT sqlserver 'sqlserver://...' AS reporting_db;

-- Query each by alias
SQL source_db SELECT count(*) FROM users;
SQL legacy_db SELECT count(*) FROM employees;
SQL reporting_db SELECT count(*) FROM reports;

-- List all connections
SQL CONNECTIONS;

-- Disconnect individually
SQL DISCONNECT legacy_db;

CLI Subcommand

For one-off queries without establishing a named connection:

mxcli sql --driver postgres --dsn 'postgres://user:pass@localhost:5432/mydb' "SELECT 1"

Security Notes

  • The DSN (which contains credentials) is never shown in session output or logs
  • Only the alias and driver type are displayed when listing connections
  • See Credential Management for environment variable and YAML config alternatives

Querying External Databases

Once connected to an external database, you can explore its schema and run arbitrary SQL queries.

Schema Exploration

Show Tables

SQL <alias> SHOW TABLES;

Lists all user tables in the connected database.

Show Views

SQL <alias> SHOW VIEWS;

Lists all user views.

Show Functions

SQL <alias> SHOW FUNCTIONS;

Lists functions and stored procedures.

Describe Table

SQL <alias> DESCRIBE <table>;

Shows column details including name, type, and nullability.

Running Queries

Any SQL statement can be passed through to the connected database:

SQL <alias> <any-sql>;

Examples

-- Connect
SQL CONNECT postgres 'postgres://user:pass@localhost:5432/mydb' AS source;

-- Explore schema
SQL source SHOW TABLES;
SQL source DESCRIBE employees;

-- Simple queries
SQL source SELECT * FROM employees WHERE active = true LIMIT 10;

-- Aggregation
SQL source SELECT department, COUNT(*) as cnt FROM employees GROUP BY department ORDER BY cnt DESC;

-- Joins
SQL source SELECT e.name, d.name AS department
  FROM employees e
  JOIN departments d ON e.dept_id = d.id
  WHERE e.active = true;

-- DDL (if you have permissions)
SQL source CREATE INDEX idx_employees_email ON employees(email);

CLI Subcommand

For one-off queries from the command line:

# Direct query
mxcli sql --driver postgres --dsn 'postgres://user:pass@localhost:5432/mydb' "SELECT * FROM users LIMIT 5"

Interactive Workflow

Within the REPL, use SQL commands interactively:

-- Connect
SQL CONNECT postgres 'postgres://user:pass@localhost:5432/mydb' AS source;

-- Explore
SQL source SHOW TABLES;
SQL source DESCRIBE users;

-- Query
SQL source SELECT id, name, email FROM users WHERE created_at > '2024-01-01' LIMIT 10;

-- Done
SQL DISCONNECT source;

Output

Query results are displayed in a formatted table by default. The output format can be controlled with session variables:

SET output_format = 'json';
SQL source SELECT * FROM users LIMIT 5;

IMPORT FROM

The IMPORT command imports data from an external database into a Mendix application’s PostgreSQL database. It supports column mapping, association linking, batch processing, and row limits.

Syntax

IMPORT FROM <alias> QUERY '<sql>'
  INTO <Module.Entity>
  MAP (<source-col> AS <AttrName> [, ...])
  [LINK (<source-col> TO <AssocName> ON <MatchAttr>) [, ...]]
  [BATCH <size>]
  [LIMIT <count>]

Parameters

ParameterDescription
<alias>Named connection established with SQL CONNECT
QUERY '<sql>'SQL query to execute against the external database
INTO <Module.Entity>Target Mendix entity to insert into
MAP (...)Column-to-attribute mapping
LINK (...)Association linking via lookup
BATCH <size>Number of rows per batch insert (default varies)
LIMIT <count>Maximum number of rows to import

Basic Import

Map source columns to entity attributes:

SQL CONNECT postgres 'postgres://user:pass@localhost:5432/mydb' AS source;

IMPORT FROM source QUERY 'SELECT name, email FROM employees'
  INTO HR.Employee
  MAP (name AS Name, email AS Email);

Import with Association Linking

The LINK clause creates associations by looking up related entities:

IMPORT FROM source QUERY 'SELECT name, dept_name FROM employees'
  INTO HR.Employee
  MAP (name AS Name)
  LINK (dept_name TO Employee_Department ON Name);

This matches the dept_name value from the source query against the Name attribute of the entity on the other side of the Employee_Department association, creating the association link for each imported row.

Batch and Limit Control

IMPORT FROM source QUERY 'SELECT * FROM products'
  INTO Catalog.Product
  MAP (name AS Name, price AS Price)
  BATCH 500
  LIMIT 1000;
  • BATCH 500 – Insert 500 rows at a time (reduces memory usage)
  • LIMIT 1000 – Stop after importing 1000 rows

How It Works

The IMPORT pipeline:

  1. Executes the source query against the external database
  2. Generates Mendix IDs for each new row (proper Mendix UUID format)
  3. Maps columns to entity attributes based on the MAP clause
  4. Resolves associations by looking up matching entities for LINK clauses
  5. Batch inserts into the Mendix application’s PostgreSQL database
  6. Tracks sequences to ensure auto-number fields continue correctly

Auto-Connection to Mendix DB

The IMPORT command automatically connects to the Mendix application’s PostgreSQL database based on the project’s configuration settings. You do not need to manually connect to the Mendix database.

Complete Example

-- Connect to external source
SQL CONNECT postgres 'postgres://user:pass@legacy-db:5432/hrdb' AS legacy;

-- Preview the data
SQL legacy SELECT name, email, department FROM employees LIMIT 5;

-- Import employees
IMPORT FROM legacy QUERY 'SELECT name, email FROM employees WHERE active = true'
  INTO HR.Employee
  MAP (name AS FullName, email AS EmailAddress)
  BATCH 500;

-- Import with association linking
IMPORT FROM legacy QUERY 'SELECT name, dept_name FROM employees WHERE active = true'
  INTO HR.Employee
  MAP (name AS FullName)
  LINK (dept_name TO Employee_Department ON DepartmentName)
  BATCH 500
  LIMIT 10000;

-- Disconnect
SQL DISCONNECT legacy;

Notes

  • The target entity must already exist in the Mendix project
  • Column types are automatically converted where possible
  • Association linking requires the target entity to have matching values in the lookup attribute
  • The Mendix application database must be a PostgreSQL instance accessible from the mxcli host

Credential Management

mxcli provides secure credential management for external database connections. Credentials are isolated from session output, logs, and error messages.

Security Principles

  • DSN isolation – Connection strings (which contain passwords) are never displayed in session output
  • Alias-only displaySQL CONNECTIONS only shows alias and driver type
  • Log sanitization – Credentials are stripped from log output and error messages

Connection Methods

Inline DSN

The simplest approach, suitable for development:

SQL CONNECT postgres 'postgres://user:password@localhost:5432/mydb' AS source;

Environment Variables

Store credentials in environment variables for CI/CD and production:

# Set environment variable
export MYDB_DSN='postgres://user:password@host:5432/mydb'

# Use in mxcli (depends on your shell expanding the variable)
SQL CONNECT postgres '$MYDB_DSN' AS source;

YAML Configuration

mxcli supports YAML configuration files for managing multiple database connections. The configuration file stores DSN information that can be referenced by name.

Configuration is resolved from:

  1. Environment variables
  2. YAML config files
  3. Inline DSN strings

CLI Subcommand

The mxcli sql subcommand accepts credentials via command-line flags:

mxcli sql --driver postgres --dsn 'postgres://user:pass@host:5432/db' "SELECT 1"

For CI/CD pipelines, use environment variables:

export DB_DSN='postgres://user:pass@host:5432/db'
mxcli sql --driver postgres --dsn "$DB_DSN" "SELECT * FROM users"

Best Practices

  1. Never commit credentials to version control
  2. Use environment variables in CI/CD pipelines
  3. Use YAML config for local development with multiple databases
  4. Rotate credentials regularly
  5. Use read-only database users when only querying (not importing)

Mendix Application Database

For IMPORT commands that write to the Mendix application database, the connection is automatically established using the project’s configuration settings (from DESCRIBE SETTINGS). The Mendix database DSN is built from the project’s DatabaseType, DatabaseUrl, DatabaseName, DatabaseUserName, and DatabasePassword settings.

Database Connector Generation

The SQL GENERATE CONNECTOR command auto-generates Mendix Database Connector MDL from an external database schema. This creates the entities, microflows, and configuration needed to query external databases from within a Mendix application.

Syntax

SQL <alias> GENERATE CONNECTOR INTO <module> [TABLES (<table1>, <table2>, ...)] [VIEWS (<view1>, ...)] [EXEC]

Parameters

ParameterDescription
<alias>Named connection established with SQL CONNECT
INTO <module>Target Mendix module to generate the connector in
TABLES (...)Optional: specific tables to include (default: all)
VIEWS (...)Optional: specific views to include
EXECExecute the generated MDL immediately (otherwise just output it)

Examples

Generate for All Tables

SQL CONNECT postgres 'postgres://user:pass@localhost:5432/mydb' AS source;

-- Preview the generated MDL (output only, no changes)
SQL source GENERATE CONNECTOR INTO HRModule;

Generate for Specific Tables

-- Generate for selected tables only
SQL source GENERATE CONNECTOR INTO HRModule TABLES (employees, departments);

Generate and Execute

-- Generate and immediately apply to the project
SQL source GENERATE CONNECTOR INTO HRModule
  TABLES (employees, departments)
  EXEC;

The EXEC flag executes the generated MDL against the connected project, creating the entities and microflows in one step.

What Gets Generated

The connector generation inspects the external database schema and produces:

  • Non-persistent entities for each table/view, with attributes matching column types
  • Microflow activities using EXECUTE DATABASE QUERY for querying the external database
  • SQL type mapping from database column types to Mendix attribute types

Type Mapping

External database types are mapped to Mendix types:

SQL TypeMendix Type
VARCHAR, TEXT, CHARString
INTEGER, INT, SMALLINTInteger
BIGINTLong
NUMERIC, DECIMAL, FLOAT, DOUBLEDecimal
BOOLEAN, BITBoolean
DATE, TIMESTAMP, DATETIMEDateTime
BYTEA, BLOBBinary

Workflow

-- 1. Connect to the external database
SQL CONNECT postgres 'postgres://user:pass@host:5432/db' AS source;

-- 2. Explore the schema
SQL source SHOW TABLES;
SQL source DESCRIBE employees;

-- 3. Preview the generated MDL
SQL source GENERATE CONNECTOR INTO HRModule TABLES (employees, departments);

-- 4. Review the output, then execute
SQL source GENERATE CONNECTOR INTO HRModule TABLES (employees, departments) EXEC;

-- 5. Disconnect
SQL DISCONNECT source;

Docker Integration

mxcli provides Docker integration for building, running, and validating Mendix applications without a local Mendix installation. Docker is also required for the testing framework.

Features

CommandDescription
mxcli docker buildBuild a Mendix application with mxbuild in Docker, including PAD patching
mxcli docker runRun a Mendix application in a Docker container
mxcli docker checkValidate a project with mx check (auto-downloads mxbuild)
mxcli testRun test files (uses Docker for mx create-project and mx check)
mxcli oqlQuery a running Mendix runtime via M2EE admin API

Prerequisites

  • Docker must be installed and running
  • The Docker daemon must be accessible from the command line

The mx Tool

The mx command-line tool validates and builds Mendix projects. mxcli can auto-download the correct version:

# Auto-download mxbuild for the project's Mendix version
mxcli setup mxbuild -p app.mpr

The mx binary location depends on the environment:

EnvironmentPath
Dev container~/.mxcli/mxbuild/{version}/modeler/mx
Repositoryreference/mxbuild/modeler/mx

Quick Start

# Validate a project
mxcli docker check -p app.mpr

# Build a deployable package
mxcli docker build -p app.mpr

mxcli docker build

The mxcli docker build command builds a Mendix application using mxbuild in Docker, including support for PAD (Platform-Agnostic Deployment) patching.

Usage

mxcli docker build -p app.mpr

What It Does

  1. Downloads mxbuild – Automatically downloads the correct mxbuild version for your project if not already available
  2. Runs mxbuild – Executes the Mendix build toolchain in a Docker container
  3. PAD patching – Applies Platform-Agnostic Deployment patches (Phase 1) to the build output
  4. Produces artifact – Generates a deployable Mendix deployment package (MDA file)

PAD Patching

PAD (Platform-Agnostic Deployment) patching modifies the build output to be compatible with container-based deployment platforms. This is essential for deploying Mendix applications to Kubernetes, Cloud Foundry, or other container orchestrators.

The build command applies six patches automatically after MxBuild produces the PAD output:

#PatchApplies ToDescription
1Set bin/start execute permissionAll versionsZIP extraction does not preserve the +x flag. This patch runs chmod 0755 on bin/start so the container can execute the start script.
2Fix Dockerfile CMD (start.sh -> start)11.6.x onlyMxBuild 11.6.x generates CMD ["./bin/start.sh"] but the actual script is named start (no .sh extension).
3Remove config argumentAll versionsRemoves the "etc/Default" argument from the CMD line. The bin/start script accumulates args with CONFIG_FILES="$CONFIG_FILES $1", which produces a leading space (" etc/Default") that the runtime rejects. Without the argument, bin/start uses its own DEFAULT_CONF variable.
4Replace deprecated base imageAll versionsReplaces FROM openjdk:21 with FROM eclipse-temurin:21-jre. The openjdk Docker images are deprecated and no longer maintained.
5Add HEALTHCHECKAll versionsInserts a HEALTHCHECK instruction before the CMD line: `curl -f http://localhost:8080/
6Bind admin API to all interfacesAll versionsAppends admin.addresses = ["*"] to the PAD config file (etc/Default). Without this, the admin API (port 8090) only listens on localhost inside the container and is unreachable from outside even when the port is mapped.

Patches are applied idempotently – if a patch has already been applied (e.g., from a previous build), it is skipped. The build output shows the status of each patch:

Applying patches...
  [applied] Set bin/start execute permission
  [skipped] Fix Dockerfile CMD (start.sh -> start)
  [applied] Remove config arg from Dockerfile CMD
  [applied] Replace deprecated openjdk base image
  [applied] Add HEALTHCHECK instruction
  [applied] Bind admin API to all interfaces

Validation with mx check

To validate a project without building:

mxcli docker check -p app.mpr

This runs mx check against the project and reports any errors:

Checking app for errors...
The app contains: 0 errors.

Auto-Download mxbuild

mxcli automatically downloads the correct mxbuild version for your project:

# Explicit setup
mxcli setup mxbuild -p app.mpr

# Or let docker build handle it automatically
mxcli docker build -p app.mpr

The downloaded mxbuild is cached at ~/.mxcli/mxbuild/{version}/ for reuse.

mxcli docker check

The mxcli docker check command validates a Mendix project without building it. It runs the Mendix mx check tool against the project and reports errors, warnings, and deprecations.

Usage

mxcli docker check -p app.mpr

What It Does

  1. Resolves the mx binary – Looks for mx in the mxbuild directory, PATH, or cached installations at ~/.mxcli/mxbuild/*/modeler/mx
  2. Runs mx check – Executes the Mendix project checker against the .mpr file
  3. Reports results – Outputs errors, warnings, and deprecations to the console

If the project has errors, the command exits with a non-zero status code.

Auto-Download

If mxbuild is not installed locally, you can download it first:

# Explicit setup (downloads matching mxbuild version)
mxcli setup mxbuild -p app.mpr

# Then check
mxcli docker check -p app.mpr

The downloaded mxbuild (including the mx binary) is cached at ~/.mxcli/mxbuild/{version}/ for reuse.

Example Output

A clean project:

Using mx: /home/vscode/.mxcli/mxbuild/11.6.3.12345/modeler/mx
Checking project /workspaces/app/app.mpr...
The app contains: 0 errors.
Project check passed.

A project with errors:

Using mx: /home/vscode/.mxcli/mxbuild/11.6.3.12345/modeler/mx
Checking project /workspaces/app/app.mpr...
CE0001: Entity 'MyModule.Customer' has no access rules configured.
CE0463: Widget definition changed for 'DataGrid2'.
The app contains: 2 errors.
project check failed: exit status 1

When to Use

mxcli docker check is faster than a full build (mxcli docker build) because it only validates the project structure without compiling or packaging. This makes it useful for:

  • Quick validation after making MDL changes
  • Pre-build gatemxcli docker build runs mx check automatically before building (use --skip-check to bypass)
  • CI pipelines – fail fast on invalid projects before spending time on a full build
  • Iterative development – check after each MDL script execution to catch errors early

Integration with mxcli docker build

The mxcli docker build command runs mx check automatically as a pre-build step. If the check fails, the build is aborted. You can skip this with:

mxcli docker build -p app.mpr --skip-check

Integration with the TUI

When using mxcli in interactive REPL mode, the TUI can auto-check the project on file changes, giving immediate feedback on whether MDL modifications introduced errors.

mxcli docker run

The mxcli docker run command runs a Mendix application in a Docker container, providing a local runtime environment for testing and development.

Usage

mxcli docker run -p app.mpr

What It Does

  1. Builds the application if no build artifact exists
  2. Starts a Docker container with the Mendix runtime
  3. Configures the runtime with the project’s settings (database, ports, etc.)
  4. Exposes the application on the configured HTTP port

Prerequisites

  • Docker must be installed and running
  • The project must be buildable (no errors in mxcli docker check)
  • A PostgreSQL database must be available (Docker can provide one)

Runtime Configuration

The runtime uses configuration from the project’s settings. You can view and modify these with:

SHOW SETTINGS;
DESCRIBE SETTINGS;
ALTER SETTINGS CONFIGURATION 'default' DatabaseType = 'POSTGRESQL';
ALTER SETTINGS CONFIGURATION 'default' HttpPortNumber = '8080';

Checking Project Health

Validate the project before running:

# Check for errors
mxcli docker check -p app.mpr

Use with OQL

Once the application is running, you can query it with OQL:

mxcli oql -p app.mpr "SELECT * FROM Sales.Customer"

See OQL Queries for details.

OQL Queries

The mxcli oql command executes OQL (Object Query Language) queries against a running Mendix runtime via the M2EE admin API. OQL is Mendix’s query language for retrieving data from the runtime, similar to SQL but operating on Mendix entities rather than database tables.

Usage

mxcli oql -p app.mpr "SELECT * FROM Sales.Customer"

Prerequisites

  • A Mendix application must be running (via mxcli docker run or another deployment)
  • The M2EE admin API must be accessible
  • The project file is needed to resolve entity and attribute names

Query Syntax

OQL uses Mendix entity names and attribute names, not database table names:

# Select all customers
mxcli oql -p app.mpr "SELECT * FROM Sales.Customer"

# Select specific attributes
mxcli oql -p app.mpr "SELECT Name, Email FROM Sales.Customer"

# Filter with WHERE
mxcli oql -p app.mpr "SELECT * FROM Sales.Order WHERE Status = 'Open'"

# Join entities via associations
mxcli oql -p app.mpr "SELECT o.OrderNumber, c.Name FROM Sales.Order o JOIN Sales.Customer c ON o.Sales.Order_Customer = c"

# Aggregation
mxcli oql -p app.mpr "SELECT COUNT(*) FROM Sales.Customer"

Difference from Catalog Queries

FeatureOQL (mxcli oql)Catalog (SELECT FROM CATALOG.*)
Data sourceRunning Mendix runtimeProject metadata (MPR file)
QueriesInstance data (rows)Model structure (entities, microflows)
RequiresRunning applicationOnly the MPR file
LanguageOQL (Mendix-specific)SQL (SQLite)
  • Use OQL to query actual data in a running application
  • Use catalog queries to analyze the project structure and metadata

Use Cases

Verifying Imported Data

After running IMPORT FROM, verify the data was imported correctly:

mxcli oql -p app.mpr "SELECT COUNT(*) FROM HR.Employee"
mxcli oql -p app.mpr "SELECT Name, Email FROM HR.Employee LIMIT 10"

Debugging Business Logic

Check data state while debugging microflows:

mxcli oql -p app.mpr "SELECT * FROM Sales.Order WHERE Status = 'Draft'"

Data Exploration

Explore data patterns in a running application:

mxcli oql -p app.mpr "SELECT Status, COUNT(*) FROM Sales.Order GROUP BY Status"

Dev Container Setup

mxcli supports development inside VS Code Dev Containers, providing a consistent, pre-configured development environment with all tools installed.

What Is a Dev Container?

A dev container is a Docker-based development environment defined by a devcontainer.json configuration file. When you open a project in VS Code with the Dev Containers extension, it builds and starts a container with all development tools pre-installed.

mxcli in Dev Containers

When running inside a dev container, mxcli tools and the mx binary are available at known paths:

ToolPath
mxcliAvailable on PATH
mx binary~/.mxcli/mxbuild/{version}/modeler/mx
mxbuild~/.mxcli/mxbuild/{version}/

Setting Up mxbuild

To download the correct mxbuild version for your project:

mxcli setup mxbuild -p app.mpr

This downloads mxbuild to ~/.mxcli/mxbuild/{version}/ and makes the mx tool available for validation and project creation.

Validating Projects

# Find the mx binary
MX=~/.mxcli/mxbuild/*/modeler/mx

# Check a project
$MX check /path/to/app.mpr

# Create a fresh project for testing
cd /tmp/test-workspace
$MX create-project

Project Initialization

Initialize mxcli for your project inside the dev container:

# Initialize with Claude Code support
mxcli init -p app.mpr

# This creates:
# - .claude/settings.json
# - .claude/commands/
# - .claude/lint-rules/
# - .ai-context/skills/
# - CLAUDE.md
# - VS Code MDL extension (auto-installed)

Docker-in-Docker

For mxcli docker build, mxcli docker run, and mxcli test (which require Docker), the dev container must have Docker-in-Docker support enabled. This is typically configured in devcontainer.json:

{
  "features": {
    "ghcr.io/devcontainers/features/docker-in-docker:2": {}
  }
}

Typical Dev Container Workflow

  1. Open the project in VS Code
  2. VS Code prompts to reopen in dev container
  3. Inside the container, run mxcli setup mxbuild -p app.mpr
  4. Run mxcli init to set up AI assistant integration
  5. Use mxcli commands as normal (REPL, exec, lint, test, etc.)

VS Code Extension

The MDL extension for VS Code provides a rich editing experience for .mdl files, bringing IDE-level support for Mendix Definition Language. The extension communicates with the mxcli lsp language server to provide real-time diagnostics, code completion, hover documentation, and navigation.

Feature Summary

FeatureDescription
Syntax highlightingMDL keyword, string, comment, and identifier coloring
Parse diagnosticsReal-time error reporting as you type
Semantic diagnosticsReference validation on save (entities, microflows, pages)
Code completionContext-aware keyword and snippet suggestions
HoverView MDL definitions by hovering over Module.Name references
Go-to-definitionCtrl+click to open element source as a virtual document
Document outlineNavigate MDL statements via the Outline panel
FoldingCollapse statement blocks and widget trees
Context menuRun File, Run Selection, and Check File commands
Project treeBrowse modules and documents in a Mendix-specific tree view

How It Works

The extension spawns mxcli lsp --stdio as a background process and communicates via the Language Server Protocol over standard I/O. This means all analysis, completion, and navigation is performed by the same engine that powers the mxcli command line.

┌──────────────┐     stdio      ┌──────────────────┐     ┌────────────┐
│  VS Code     │◀──────────────▶│  mxcli lsp       │────▶│  .mpr file │
│  Extension   │   LSP JSON-RPC │  --stdio          │     │            │
└──────────────┘                └──────────────────┘     └────────────┘

Requirements

  • VS Code 1.80 or later (or any VS Code fork such as Cursor)
  • mxcli binary on PATH or configured via the mdl.mxcliPath setting
  • A Mendix project (.mpr file) for semantic features

Settings

SettingDefaultDescription
mdl.mxcliPathmxcliPath to the mxcli executable
mdl.mprPath(auto-discovered)Path to .mpr file; if empty, the extension searches the workspace

Getting Started

The fastest way to get started is mxcli init, which automatically installs the extension into your project. See Installation for details, or jump to specific features:

Installation

There are three ways to install the VS Code MDL extension.

Running mxcli init on a Mendix project automatically installs the VS Code extension along with skills, commands, and configuration files:

mxcli init /path/to/my-mendix-project

The command copies the bundled .vsix file into the project and installs it into VS Code. This is the recommended approach because it also sets up the full AI-assisted development environment.

Option 2: Manual VSIX Install

If you already have the VSIX file (e.g., from the mxcli build artifacts), install it directly:

code --install-extension vscode-mdl/vscode-mdl-*.vsix

Or from the VS Code Command Palette:

  1. Open Extensions: Install from VSIX… (Ctrl+Shift+P)
  2. Navigate to the .vsix file
  3. Click Install

Option 3: Build from Source

If you are developing or modifying the extension:

# Requires bun (not npm/node)
cd vscode-mdl
bun install
bun run compile

# Package as VSIX
make vscode-ext

# Install the built VSIX
make vscode-install

Note: The VS Code extension build toolchain uses bun, not npm or node. The Makefile targets make vscode-ext and make vscode-install handle this automatically.

Verifying the Installation

After installation, open any .mdl file in VS Code. You should see:

  1. Syntax highlighting – MDL keywords like CREATE, SHOW, and DESCRIBE are colored
  2. Parse diagnostics – Syntax errors appear as red underlines immediately
  3. Status bar – The MDL language server status appears in the bottom bar

Configuring the Extension

Open VS Code settings (Ctrl+,) and search for “MDL”:

SettingDefaultDescription
mdl.mxcliPathmxcliPath to the mxcli binary. Set this if mxcli is not on your PATH.
mdl.mprPath(empty)Path to the .mpr file. If empty, the extension auto-discovers it by searching the workspace root.

Example settings.json:

{
  "mdl.mxcliPath": "./mxcli",
  "mdl.mprPath": "./app.mpr"
}

Dev Container Setup

When using mxcli init, a .devcontainer/ configuration is also created. Opening the project in VS Code and choosing Reopen in Container gives you a sandboxed environment with:

ComponentPurpose
mxcliMendix CLI binary (copied into the project)
MxBuild / mxProject validation and building
JDK 21Required by MxBuild
Docker-in-DockerRunning Mendix apps locally
Node.jsPlaywright testing support
PostgreSQL clientDatabase connectivity
Claude CodeAI coding assistant (auto-installed)

The VS Code extension is automatically available inside the dev container.

Syntax Highlighting and Diagnostics

The VS Code extension provides three layers of language feedback: syntax highlighting, parse diagnostics, and semantic diagnostics.

Syntax Highlighting

MDL files (.mdl) receive full syntax coloring based on a TextMate grammar:

Token TypeExamplesColor Theme Category
KeywordsCREATE, SHOW, DESCRIBE, ALTER, GRANTkeyword
TypesString, Integer, Boolean, DateTimesupport.type
Strings'Hello World'string
Numbers100, 3.14constant.numeric
Comments-- line comment, /** doc comment */comment
IdentifiersMyModule.Customervariable
Operators=, !=, AND, ORkeyword.operator

Highlighting works immediately on file open – no project or language server connection is required.

Parse Diagnostics (Real-Time)

As you type, the extension sends the file content to the MDL language server, which runs the ANTLR4 parser and reports syntax errors in real time. These appear as:

  • Red underlines on the offending tokens
  • Error entries in the Problems panel (Ctrl+Shift+M)
  • Inline hover messages when you point at the underlined text

Parse diagnostics catch issues like:

  • Missing semicolons
  • Unmatched parentheses or braces
  • Invalid keyword combinations
  • Malformed qualified names

Example error:

Line 3, Col 42: mismatched input ')' expecting ','

Parse diagnostics run on every keystroke (debounced) and do not require a Mendix project file.

Semantic Diagnostics (On Save)

When you save a .mdl file, the language server performs deeper validation against the actual Mendix project (.mpr file). Semantic diagnostics check:

  • Entity references – does MyModule.Customer actually exist?
  • Attribute references – does the entity have the named attribute?
  • Microflow references – do called microflows exist with correct signatures?
  • Association references – are FROM/TO entities valid?
  • Module existence – does the referenced module exist in the project?

Semantic diagnostics require:

  1. A valid mdl.mprPath setting (or auto-discovered .mpr file)
  2. The mxcli lsp server to be running

These diagnostics appear as warnings or errors in the Problems panel, identical in appearance to parse errors but typically with more descriptive messages referencing the project state.

Diagnostic Severity Levels

SeverityMeaning
ErrorSyntax error or invalid reference that would prevent execution
WarningValid syntax but potentially problematic (e.g., unused variable)
InformationSuggestion or style recommendation

Equivalent CLI Commands

The same checks available in the extension can be run from the command line:

# Syntax check only (no project needed)
mxcli check script.mdl

# Syntax + reference validation
mxcli check script.mdl -p app.mpr --references

Completion, Hover, Go-to-Definition

The VS Code extension provides IntelliSense features powered by the MDL language server (mxcli lsp --stdio).

Code Completion

Press Ctrl+Space or start typing to trigger context-aware completions:

Keyword Completion

At the start of a statement, the extension suggests top-level MDL keywords:

CREATE  SHOW  DESCRIBE  ALTER  DROP  GRANT  REVOKE  SEARCH  REFRESH  ...

After a keyword, context-appropriate follow-up keywords appear:

CREATE |
       ├── ENTITY
       ├── ASSOCIATION
       ├── ENUMERATION
       ├── MICROFLOW
       ├── PAGE
       ├── SNIPPET
       ├── MODULE
       ├── MODULE ROLE
       ├── USER ROLE
       └── WORKFLOW

Snippet Completion

Common patterns are offered as multi-line snippets. For example, typing entity and selecting the snippet inserts:

CREATE PERSISTENT ENTITY ${1:Module}.${2:EntityName} (
    ${3:Name}: ${4:String(200)}
);

Reference Completion

When the language server has a loaded project, qualified name references offer completions from the actual model:

  • Entity names after FROM, TO, and in data source expressions
  • Module names after IN and as the first part of qualified names
  • Attribute names inside entity contexts
  • Microflow names after CALL MICROFLOW

Hover

Hover over any Module.Name qualified reference to see its MDL definition in a tooltip. For example, hovering over Sales.Customer in:

RETRIEVE $c FROM Sales.Customer WHERE ...

Displays the entity definition:

CREATE PERSISTENT ENTITY Sales.Customer (
    Name: String(200) NOT NULL,
    Email: String(200),
    IsActive: Boolean DEFAULT true
);

Hover works for entities, microflows, pages, enumerations, associations, and snippets.

Go-to-Definition

Ctrl+Click (or F12) on a qualified reference opens the element’s MDL source as a virtual read-only document. The definition is generated on-the-fly by describing the element from the project.

For example, Ctrl+clicking Sales.ProcessOrder opens a virtual document showing the full microflow definition with all its activities, conditions, and flows.

This also works from the terminal: qualified names like MyModule.EntityName in mxcli output are rendered as clickable links that open the definition.

Document Symbols and Outline

The Outline panel (View > Outline) shows all MDL statements in the current file as a navigable tree:

├── CREATE ENTITY Sales.Customer
├── CREATE ENTITY Sales.Order
├── CREATE ASSOCIATION Sales.Order_Customer
├── CREATE MICROFLOW Sales.ProcessOrder
└── CREATE PAGE Sales.CustomerOverview

Click any entry to jump to that statement in the editor.

Folding

Statement blocks can be collapsed:

  • CREATE MICROFLOW ... BEGIN ... END; – fold the microflow body
  • CREATE PAGE ... { ... } – fold the widget tree
  • IF ... THEN ... END IF; – fold conditional blocks
  • LOOP ... BEGIN ... END LOOP; – fold loop bodies
  • /** ... */ – fold documentation comments

Project Tree

The VS Code extension provides a Mendix Project Tree view that mirrors the App Explorer in Mendix Studio Pro, allowing you to browse modules and documents without leaving VS Code.

Accessing the Tree

The project tree appears in the Explorer sidebar under the “Mendix Project” section. If it does not appear, check that:

  1. The extension is installed and enabled
  2. A .mpr file is present in the workspace (or configured via mdl.mprPath)
  3. The mxcli binary is accessible

Tree Structure

The tree is organized by module, with documents grouped by type:

Mendix Project
├── MyFirstModule
│   ├── Domain Model
│   │   ├── Customer
│   │   ├── Order
│   │   └── Product
│   ├── Microflows
│   │   ├── ACT_Customer_Create
│   │   ├── ACT_Customer_Delete
│   │   └── ProcessOrder
│   ├── Pages
│   │   ├── Customer_Overview
│   │   ├── Customer_NewEdit
│   │   └── Order_Overview
│   ├── Snippets
│   │   └── NavigationMenu
│   ├── Enumerations
│   │   └── OrderStatus
│   └── Nanoflows
│       └── NF_ValidateInput
├── Administration
│   └── ...
└── System
    └── ...

Interactions

  • Click a document to open its MDL source as a virtual read-only document (equivalent to DESCRIBE in the REPL)
  • Expand a module to see all document types
  • Expand a document type to see individual documents

Refreshing

The tree refreshes automatically when the project file changes. You can also trigger a manual refresh from the tree view toolbar.

Relationship to SHOW Commands

The tree is backed by the same metadata that powers the SHOW commands:

Tree LevelEquivalent MDL Command
Module listSHOW MODULES
Entities in a moduleSHOW ENTITIES IN MyModule
Microflows in a moduleSHOW MICROFLOWS IN MyModule
Pages in a moduleSHOW PAGES IN MyModule
Full structureSHOW STRUCTURE IN MyModule DEPTH 2

Context Menu Commands

The VS Code extension adds three commands to the editor context menu (right-click) for .mdl files. These commands provide quick access to running and validating MDL without switching to a terminal.

Run File

Right-click > MDL: Run File

Executes the entire .mdl file against the configured Mendix project. This is equivalent to:

mxcli exec script.mdl -p app.mpr

Output appears in the integrated terminal. Any errors from execution (missing references, invalid operations) are displayed inline.

Check File

Right-click > MDL: Check File

Validates the file’s syntax without executing it. This is equivalent to:

mxcli check script.mdl

If a project is configured, reference validation is also performed:

mxcli check script.mdl -p app.mpr --references

Check results appear in the Problems panel alongside the live diagnostics from the language server.

Run Selection

Right-click > MDL: Run Selection

Executes only the currently selected text as MDL. This is useful for:

  • Running a single CREATE statement from a larger script
  • Testing a SHOW or DESCRIBE command interactively
  • Executing a REFRESH CATALOG before running queries

The selection must contain one or more complete MDL statements (terminated by ;).

Keyboard Shortcuts

The commands do not have default keyboard shortcuts, but you can bind them in VS Code:

  1. Open Keyboard Shortcuts (Ctrl+K Ctrl+S)
  2. Search for “MDL”
  3. Assign your preferred key bindings

Terminal Integration

All commands use the VS Code integrated terminal. Output from mxcli includes clickable links for qualified names – Ctrl+click on MyModule.EntityName in the terminal output to open the element’s definition.

LSP Server

The MDL language server implements the Language Server Protocol (LSP), enabling any editor with LSP support to provide rich MDL editing features. The server is built into the mxcli binary – no separate installation is needed.

Overview

The language server is started with:

mxcli lsp --stdio

It communicates over standard I/O using JSON-RPC, the standard LSP transport. The server provides:

  • Real-time diagnostics from the ANTLR4 parser
  • Semantic diagnostics validated against the Mendix project
  • Code completion with keywords, snippets, and project references
  • Hover showing MDL definitions for qualified names
  • Go-to-definition opening element source as virtual documents
  • Document symbols for outline and breadcrumb navigation
  • Folding ranges for collapsible statement blocks

Architecture

┌──────────────┐                    ┌─────────────────────┐
│    Editor     │◀── LSP JSON-RPC ─▶│   mxcli lsp         │
│  (VS Code,   │     over stdio     │                     │
│   Neovim,    │                    │  ┌───────────────┐  │
│   Emacs)     │                    │  │ ANTLR4 Parser │  │
└──────────────┘                    │  └───────────────┘  │
                                    │  ┌───────────────┐  │
                                    │  │ MPR Reader    │  │
                                    │  └───────────────┘  │
                                    │  ┌───────────────┐  │
                                    │  │ Catalog       │  │
                                    │  └───────────────┘  │
                                    └─────────────────────┘

The server is implemented in cmd/mxcli/lsp.go. It reuses the same ANTLR4 parser, MPR reader, and catalog components that power the mxcli CLI.

Project Discovery

On startup, the server auto-discovers the Mendix project:

  1. Check the mdl.mprPath client configuration
  2. Search the workspace root for *.mpr files
  3. If found, load the project for semantic features

If no project is found, the server still provides syntax highlighting and parse diagnostics, but semantic features (reference validation, hover definitions, completion from project) are disabled.

Protocol

The MDL language server uses the standard LSP stdio transport: JSON-RPC messages are exchanged over the process’s standard input and standard output.

Starting the Server

mxcli lsp --stdio

The --stdio flag is required. The server reads LSP requests from stdin and writes responses to stdout. Diagnostic logging goes to stderr.

Initialization Handshake

The client initiates the connection with an initialize request. The server responds with its capabilities:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "capabilities": {},
    "rootUri": "file:///path/to/project",
    "initializationOptions": {
      "mprPath": "app.mpr"
    }
  }
}

The initializationOptions.mprPath is optional. If provided, the server uses it to locate the Mendix project. Otherwise, it searches the rootUri for .mpr files.

The server responds with:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "capabilities": {
      "textDocumentSync": 1,
      "completionProvider": { "triggerCharacters": [".", " "] },
      "hoverProvider": true,
      "definitionProvider": true,
      "documentSymbolProvider": true,
      "foldingRangeProvider": true,
      "diagnosticProvider": {
        "interFileDependencies": false,
        "workspaceDiagnostics": false
      }
    }
  }
}

After receiving the response, the client sends initialized to complete the handshake.

Document Synchronization

The server uses full document sync (textDocumentSync: 1). On every change, the client sends the complete document content:

  • textDocument/didOpen – file opened, triggers initial parse
  • textDocument/didChange – content changed, triggers re-parse and diagnostics
  • textDocument/didSave – file saved, triggers semantic validation
  • textDocument/didClose – file closed, clears diagnostics

Diagnostics

Diagnostics are pushed from the server to the client via textDocument/publishDiagnostics notifications. Two levels of diagnostics are reported:

  1. Parse diagnostics – sent after every didChange, based on ANTLR4 parser errors
  2. Semantic diagnostics – sent after didSave, based on reference validation against the project

Shutdown

The client sends shutdown followed by exit to terminate the server cleanly.

Capabilities

The MDL language server supports the following LSP capabilities.

Supported Methods

LSP MethodFeatureNotes
textDocument/publishDiagnosticsParse and semantic error reportingPush-based; parse on change, semantic on save
textDocument/completionCode completionKeywords, snippets, and project references
textDocument/hoverHover documentationMDL definitions for qualified names
textDocument/definitionGo-to-definitionOpens element source as virtual document
textDocument/documentSymbolDocument outlineAll MDL statements in the file
textDocument/foldingRangeCode foldingStatement blocks, widget trees, comments

Document Synchronization

MethodSupported
textDocument/didOpenYes
textDocument/didChangeYes (full sync)
textDocument/didSaveYes
textDocument/didCloseYes

Completion Details

Trigger characters: . and (space).

Completion items include:

  • KeywordsCREATE, SHOW, DESCRIBE, ALTER, DROP, GRANT, etc.
  • Keyword sequencesPERSISTENT ENTITY, MODULE ROLE, USER ROLE
  • Data typesString, Integer, Boolean, DateTime, Decimal, etc.
  • Snippets – Multi-line templates for entity, microflow, page creation
  • Project references – Module names, entity names, microflow names (requires project)

Diagnostics Details

Parse Diagnostics (on change)

  • Syntax errors from the ANTLR4 lexer and parser
  • Missing tokens, unexpected tokens, mismatched brackets
  • Reported with exact line/column positions

Semantic Diagnostics (on save)

  • Unresolved entity, microflow, or page references
  • Invalid attribute names on known entities
  • Module existence checks
  • Requires a loaded Mendix project

Not Yet Supported

The following LSP capabilities are not currently implemented:

LSP MethodStatus
textDocument/referencesPlanned
textDocument/renamePlanned
textDocument/formattingPlanned
textDocument/codeActionPlanned
textDocument/signatureHelpNot planned
workspace/symbolNot planned

Integration with Other Editors

The MDL language server works with any editor that supports the Language Server Protocol. This page covers setup for editors other than VS Code.

General Setup

All editors need to:

  1. Start mxcli lsp --stdio as a subprocess
  2. Communicate via JSON-RPC over stdin/stdout
  3. Associate the language server with .mdl files

Neovim

Using nvim-lspconfig

Add the following to your Neovim configuration:

local lspconfig = require('lspconfig')
local configs = require('lspconfig.configs')

-- Define the MDL language server
if not configs.mdl then
  configs.mdl = {
    default_config = {
      cmd = { 'mxcli', 'lsp', '--stdio' },
      filetypes = { 'mdl' },
      root_dir = function(fname)
        return lspconfig.util.find_git_ancestor(fname)
          or lspconfig.util.root_pattern('*.mpr')(fname)
      end,
      settings = {},
    },
  }
end

lspconfig.mdl.setup({})

File Type Detection

Add MDL file type recognition:

vim.filetype.add({
  extension = {
    mdl = 'mdl',
  },
})

Syntax Highlighting

For basic syntax highlighting without a full TreeSitter grammar, create ~/.config/nvim/syntax/mdl.vim with keyword highlights for MDL reserved words, or use the TextMate grammar from the VS Code extension via a plugin like nvim-textmate.

Emacs

Using lsp-mode

(require 'lsp-mode)

(add-to-list 'lsp-language-id-configuration '(mdl-mode . "mdl"))

(lsp-register-client
 (make-lsp-client
  :new-connection (lsp-stdio-connection '("mxcli" "lsp" "--stdio"))
  :activation-fn (lsp-activate-on "mdl")
  :server-id 'mdl-ls))

(add-hook 'mdl-mode-hook #'lsp)

Using eglot (Built-in since Emacs 29)

(add-to-list 'eglot-server-programs
             '(mdl-mode . ("mxcli" "lsp" "--stdio")))

(add-hook 'mdl-mode-hook #'eglot-ensure)

File Mode

Define a basic major mode for .mdl files:

(define-derived-mode mdl-mode prog-mode "MDL"
  "Major mode for editing MDL (Mendix Definition Language) files."
  (setq-local comment-start "-- ")
  (setq-local comment-end ""))

(add-to-list 'auto-mode-alist '("\\.mdl\\'" . mdl-mode))

Sublime Text

Using LSP Package

Install the LSP package, then add to your LSP settings:

{
  "clients": {
    "mdl": {
      "enabled": true,
      "command": ["mxcli", "lsp", "--stdio"],
      "selector": "source.mdl",
      "schemes": ["file"]
    }
  }
}

Helix

Add to ~/.config/helix/languages.toml:

[[language]]
name = "mdl"
scope = "source.mdl"
file-types = ["mdl"]
language-servers = ["mdl-ls"]
comment-token = "--"

[language-server.mdl-ls]
command = "mxcli"
args = ["lsp", "--stdio"]

Generic LSP Client

For any editor with LSP support, the key configuration values are:

ParameterValue
Commandmxcli lsp --stdio
Transportstdio
Language IDmdl
File extensions.mdl
Trigger characters.,

mxcli init

The mxcli init command prepares a Mendix project for AI-assisted development. It creates configuration files, installs skills, sets up a dev container, and optionally installs the VS Code extension.

Basic Usage

# Initialize with Claude Code (default)
mxcli init /path/to/my-mendix-project

# Initialize for a specific AI tool
mxcli init --tool cursor /path/to/my-mendix-project

# Initialize for multiple tools
mxcli init --tool claude --tool cursor /path/to/my-mendix-project

# Initialize for all supported tools
mxcli init --all-tools /path/to/my-mendix-project

# List supported tools
mxcli init --list-tools

Supported AI Tools

ToolFlagConfig Files Created
Claude Code (default)--tool claude.claude/settings.json, CLAUDE.md, commands, lint rules
Cursor--tool cursor.cursorrules
Continue.dev--tool continue.continue/config.json
Windsurf--tool windsurf.windsurfrules
Aider--tool aider.aider.conf.yml

All tools also receive the universal files (AGENTS.md, .ai-context/).

What Happens During Init

  1. Detect the Mendix project – finds the .mpr file in the target directory
  2. Create universal AI contextAGENTS.md and .ai-context/skills/ with MDL pattern guides
  3. Create tool-specific configuration – based on the selected --tool flag(s)
  4. Set up dev container.devcontainer/ with Dockerfile and configuration
  5. Copy mxcli binary – places the mxcli executable in the project root
  6. Install VS Code extension – copies and installs the bundled .vsix file

Adding a Tool Later

To add support for an additional AI tool to an existing project:

mxcli add-tool cursor

This creates only the tool-specific files without overwriting existing configuration.

What Gets Created

Running mxcli init creates the following directory structure in your Mendix project.

Universal Files (All Tools)

These files are shared by all AI tools:

your-mendix-project/
├── AGENTS.md                          # Comprehensive AI assistant guide
├── .ai-context/
│   ├── skills/                        # MDL pattern guides
│   │   ├── write-microflows.md        # Microflow syntax and patterns
│   │   ├── create-page.md            # Page/widget syntax reference
│   │   ├── alter-page.md             # ALTER PAGE in-place modifications
│   │   ├── overview-pages.md         # CRUD page patterns
│   │   ├── master-detail-pages.md    # Master-detail page patterns
│   │   ├── generate-domain-model.md  # Entity/Association syntax
│   │   ├── check-syntax.md           # Pre-flight validation checklist
│   │   ├── organize-project.md       # Folders, MOVE, project structure
│   │   ├── manage-security.md        # Roles, access control, GRANT/REVOKE
│   │   ├── manage-navigation.md      # Navigation profiles, menus
│   │   ├── demo-data.md              # Database/import/demo data
│   │   ├── xpath-constraints.md      # XPath syntax in WHERE clauses
│   │   ├── database-connections.md   # External database connections
│   │   ├── test-microflows.md        # Test annotations and Docker setup
│   │   └── patterns-data-processing.md # Delta merge, batch processing
│   └── examples/                      # Example MDL scripts
├── mxcli                              # CLI executable (copied)
└── .devcontainer/                     # Dev container configuration
    ├── devcontainer.json
    └── Dockerfile

Tool-Specific Files

Claude Code

.claude/
├── settings.json          # Claude Code project settings
├── commands/              # Slash commands for Claude
│   └── mendix/           # Mendix-specific commands
└── lint-rules/            # Starlark lint rules
CLAUDE.md                  # Project context for Claude

Cursor

.cursorrules               # Compact MDL reference and command guide

Continue.dev

.continue/
└── config.json            # Custom commands and slash commands

Windsurf

.windsurfrules             # MDL rules for Codeium's AI

Aider

.aider.conf.yml            # YAML configuration for Aider

Dev Container Contents

The .devcontainer/ provides a sandboxed environment with:

ComponentPurpose
mxcliMendix CLI binary
MxBuild / mxProject validation and building (auto-downloaded on first use)
JDK 21 (Adoptium)Required by MxBuild
Docker-in-DockerRunning Mendix apps locally with mxcli docker
Node.jsPlaywright testing support
PostgreSQL clientDatabase connectivity
Claude CodeAI coding assistant (auto-installed on container creation)

Key paths inside the container:

~/.mxcli/mxbuild/{version}/modeler/mx    # mx check / mx build
~/.mxcli/runtime/{version}/               # Mendix runtime (auto-downloaded)
./mxcli                                    # Project-local mxcli binary

Skills Overview

The skill files in .ai-context/skills/ teach AI assistants how to generate correct MDL. Each skill covers a specific topic:

Skill FileWhat It Teaches
write-microflows.mdMicroflow syntax, common mistakes, validation checklist
create-page.mdPage/widget syntax reference
overview-pages.mdCRUD page patterns (data grids, search, buttons)
generate-domain-model.mdEntity, attribute, association, enumeration syntax
manage-security.mdModule roles, user roles, GRANT/REVOKE patterns
demo-data.mdMendix ID system, association storage, data insertion
check-syntax.mdPre-flight validation checklist before execution

AI assistants are instructed to read the relevant skill file before generating MDL for that topic.

Customizing Skills

The skill files generated by mxcli init are starting points. You can modify them to match your organization’s patterns, naming conventions, and architectural standards.

Skill File Location

Skills are stored in .ai-context/skills/ (universal, shared by all tools) and optionally in .claude/skills/ (Claude Code-specific).

Modifying Existing Skills

Edit any skill file to adjust the patterns the AI follows. Common customizations include:

Naming Conventions

Change the default naming patterns in generate-domain-model.md:

-- Default pattern
CREATE PERSISTENT ENTITY Module.Customer ( ... );

-- Your convention: prefix entities with 'tbl_'
CREATE PERSISTENT ENTITY Module.tbl_Customer ( ... );

Page Layout Standards

Modify create-page.md or overview-pages.md to use your organization’s standard layouts:

-- Default
CREATE PAGE Module.CustomerOverview (
  Layout: Atlas_Core.Atlas_Default
) { ... }

-- Your standard layout
CREATE PAGE Module.CustomerOverview (
  Layout: MyTheme.MainLayout
) { ... }

Security Patterns

Update manage-security.md with your standard role hierarchy:

-- Your organization's standard roles
CREATE MODULE ROLE Module.SystemAdmin DESCRIPTION 'Full system access';
CREATE MODULE ROLE Module.PowerUser DESCRIPTION 'Extended user access';
CREATE MODULE ROLE Module.StandardUser DESCRIPTION 'Normal user access';
CREATE MODULE ROLE Module.ReadOnly DESCRIPTION 'View-only access';

Adding New Skills

Create new .md files in .ai-context/skills/ for custom patterns:

.ai-context/skills/
├── write-microflows.md          # Built-in
├── create-page.md               # Built-in
├── my-api-integration.md        # Custom: your REST API patterns
├── my-approval-workflow.md      # Custom: approval workflow template
└── my-reporting-pattern.md      # Custom: standard report pages

Skill File Structure

A good skill file includes:

  1. Title and purpose – what the skill covers
  2. Syntax reference – the MDL syntax for the topic
  3. Complete examples – working MDL that can be adapted
  4. Common mistakes – pitfalls to avoid
  5. Validation checklist – steps to verify correctness

Example custom skill:

# REST API Integration Pattern

## When to Use
When creating microflows that call external REST APIs.

## Pattern

\```sql
CREATE MICROFLOW Module.Call_ExternalAPI($param: String)
RETURNS String AS $response
FOLDER 'Integration'
BEGIN
    -- Your standard REST call pattern here
    ...
END;
\```

## Common Mistakes
- Forgetting error handling for HTTP failures
- Not logging the request/response for debugging

Claude Code Commands

For Claude Code specifically, you can also add slash commands in .claude/commands/mendix/:

.claude/commands/mendix/
├── create-entity.md
├── create-overview.md
└── your-custom-command.md

These appear as /mendix:your-custom-command in the Claude Code interface.

Syncing with Updates

When you upgrade mxcli to a newer version, the skills, commands, and VS Code extension bundled with it may have changed. This page explains how to keep your project’s files in sync.

What Gets Updated

ComponentSource of TruthSync Target
Skillsreference/mendix-repl/templates/.claude/skills/.ai-context/skills/
Commands.claude/commands/mendix/.claude/commands/mendix/
VS Code extensionvscode-mdl/vscode-mdl-*.vsixInstalled extension
Lint rulesBundled Starlark rules.claude/lint-rules/

Re-running mxcli init

The simplest way to sync is to re-run mxcli init:

mxcli init /path/to/my-mendix-project

This overwrites the generated skill files with the latest versions. Custom modifications to built-in skill files will be lost. To preserve customizations:

  1. Keep custom skills in separate files (e.g., my-custom-pattern.md)
  2. Or use version control to merge changes

Build-Time Sync (for mxcli developers)

When building mxcli from source, make build automatically syncs all assets:

make build          # Syncs everything
make sync-skills    # Skills only
make sync-commands  # Commands only
make sync-vsix      # VS Code extension only

Checking Versions

To see which version of mxcli and its bundled assets you have:

mxcli version
  1. Keep custom skills separate from built-in skills so re-syncing does not overwrite them
  2. Use version control for your .ai-context/ and .claude/ directories
  3. Re-run mxcli init after upgrading mxcli to pick up new skills and bug fixes
  4. Review the diff after syncing to see what changed in the skill files

Quick Start

The modelsdk-go library provides programmatic access to Mendix projects from Go code. It is the underlying library that powers the mxcli CLI tool.

Overview

  • Read Mendix project files (.mpr) to inspect modules, entities, microflows, pages, and more
  • Write changes back to projects: create entities, attributes, associations, microflows, pages
  • No cloud connectivity required – works directly with local .mpr files
  • No CGO required – uses a pure Go SQLite driver (modernc.org/sqlite)
  • Automatic format detection – handles both MPR v1 (single file) and v2 (directory-based) formats

Minimal Example

package main

import (
    "fmt"
    "github.com/mendixlabs/mxcli"
)

func main() {
    reader, err := modelsdk.Open("/path/to/MyApp.mpr")
    if err != nil {
        panic(err)
    }
    defer reader.Close()

    modules, _ := reader.ListModules()
    for _, m := range modules {
        fmt.Printf("Module: %s\n", m.Name)
    }
}

Two Access Modes

ModeFunctionUse Case
Read-onlymodelsdk.Open(path)Inspecting project structure, generating reports
Read-writemodelsdk.OpenForWriting(path)Creating entities, microflows, pages

Two API Levels

LevelPackageStyle
Low-level SDKmodelsdk (root)Direct type construction and writer calls
High-level Fluent APIapi/Builder pattern with method chaining

The low-level SDK gives full control over model elements. The fluent API provides a simplified interface for common operations.

MPR File Format

Mendix projects are stored in .mpr files which are SQLite databases containing BSON-encoded model elements.

  • MPR v1 (Mendix < 10.18): Single .mpr file containing all model data
  • MPR v2 (Mendix >= 10.18): .mpr metadata file + mprcontents/ folder with individual documents

The library automatically detects and handles both formats.

Model Structure

Project
├── Modules
│   ├── Domain Model
│   │   ├── Entities
│   │   │   ├── Attributes
│   │   │   ├── Indexes
│   │   │   ├── Access Rules
│   │   │   ├── Validation Rules
│   │   │   └── Event Handlers
│   │   ├── Associations
│   │   └── Annotations
│   ├── Microflows
│   │   ├── Parameters
│   │   └── Activities & Flows
│   ├── Nanoflows
│   ├── Pages
│   │   ├── Widgets
│   │   └── Data Sources
│   ├── Layouts
│   ├── Snippets
│   ├── Enumerations
│   ├── Constants
│   ├── Scheduled Events
│   └── Java Actions
└── Project Documents

Comparison with Official SDK

FeatureMendix Model SDK (TypeScript)modelsdk-go
LanguageTypeScript/JavaScriptGo
RuntimeNode.jsNative binary
Cloud RequiredYes (Platform API)No
Local FilesNoYes
Real-time CollaborationYesNo
Read OperationsYesYes
Write OperationsYesYes
Type SafetyYes (TypeScript)Yes (Go)
CLI ToolNoYes (mxcli)
SQL-like DSLNoYes (MDL)

Installation

Go Module

Install the library with go get:

go get github.com/mendixlabs/mxcli

Requirements

  • Go 1.21+
  • No CGO required – the project uses modernc.org/sqlite (pure Go SQLite) and does not require a C compiler

Dependencies

The library pulls in the following key dependencies:

DependencyPurpose
modernc.org/sqlitePure Go SQLite driver for reading .mpr files
go.mongodb.org/mongo-driverBSON parsing for Mendix document format

These are resolved automatically by go get.

Verify Installation

Create a simple Go program to verify the library is working:

package main

import (
    "fmt"
    "github.com/mendixlabs/mxcli"
)

func main() {
    reader, err := modelsdk.Open("/path/to/MyApp.mpr")
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        return
    }
    defer reader.Close()

    version := reader.GetMendixVersion()
    fmt.Printf("Mendix version: %s\n", version)
    fmt.Printf("MPR format: v%d\n", reader.Version())
}

Run it:

go run main.go

Building from Source

If you want to build the mxcli CLI tool from source:

git clone https://github.com/mendixlabs/mxcli.git
cd mxcli
make build

The binary will be placed in bin/mxcli.

Reading a Project

Open a Mendix project file (.mpr) in read-only mode to inspect its structure and contents.

Opening a Project

reader, err := modelsdk.Open("/path/to/MyApp.mpr")
if err != nil {
    panic(err)
}
defer reader.Close()

The library automatically detects the MPR format version (v1 or v2) and handles both transparently.

Complete Example

package main

import (
    "fmt"
    "github.com/mendixlabs/mxcli"
)

func main() {
    // Open a Mendix project
    reader, err := modelsdk.Open("/path/to/MyApp.mpr")
    if err != nil {
        panic(err)
    }
    defer reader.Close()

    // List all modules
    modules, _ := reader.ListModules()
    for _, m := range modules {
        fmt.Printf("Module: %s\n", m.Name)
    }

    // Get domain model for a module
    dm, _ := reader.GetDomainModel(modules[0].ID)
    for _, entity := range dm.Entities {
        fmt.Printf("  Entity: %s\n", entity.Name)
        for _, attr := range entity.Attributes {
            fmt.Printf("    - %s: %s\n", attr.Name, attr.Type.GetTypeName())
        }
    }

    // List microflows
    microflows, _ := reader.ListMicroflows()
    fmt.Printf("Total microflows: %d\n", len(microflows))

    // List pages
    pages, _ := reader.ListPages()
    fmt.Printf("Total pages: %d\n", len(pages))
}

Reader Methods

Metadata

reader.Path()                    // Get file path
reader.Version()                 // Get MPR version (1 or 2)
reader.GetMendixVersion()        // Get Mendix Studio Pro version

Modules

reader.ListModules()             // List all modules
reader.GetModule(id)             // Get module by ID
reader.GetModuleByName(name)     // Get module by name

Domain Models

reader.ListDomainModels()        // List all domain models
reader.GetDomainModel(moduleID)  // Get domain model for module

Microflows and Nanoflows

reader.ListMicroflows()          // List all microflows
reader.GetMicroflow(id)          // Get microflow by ID
reader.ListNanoflows()           // List all nanoflows
reader.GetNanoflow(id)           // Get nanoflow by ID

Pages and Layouts

reader.ListPages()               // List all pages
reader.GetPage(id)               // Get page by ID
reader.ListLayouts()             // List all layouts
reader.GetLayout(id)             // Get layout by ID

Other Elements

reader.ListEnumerations()        // List all enumerations
reader.ListConstants()           // List all constants
reader.ListScheduledEvents()     // List all scheduled events
reader.ExportJSON()              // Export entire model as JSON

Running the Example

cd examples/read_project
go run main.go /path/to/MyApp.mpr

Data Flow

sequenceDiagram
    participant App
    participant SDK as modelsdk
    participant Reader
    participant Parser
    participant FS as File System

    App->>SDK: Open("project.mpr")
    SDK->>Reader: NewReader(path)
    Reader->>FS: Detect MPR version
    alt MPR v1
        Reader->>FS: Open SQLite database
    else MPR v2
        Reader->>FS: Read mprcontents/
    end
    SDK-->>App: Reader

    App->>SDK: ListDomainModels()
    SDK->>Reader: listUnitsByType("DomainModels$DomainModel")
    Reader->>FS: Read unit contents
    Reader->>Parser: parseDomainModel(bson)
    Parser-->>Reader: *DomainModel
    Reader-->>SDK: []*DomainModel
    SDK-->>App: []*DomainModel

Units are loaded on-demand for performance. The reader uses lazy loading so that related documents are only parsed when accessed.

Modifying a Project

Make changes to a Mendix project file using the writer API. Always back up your .mpr file before modifying it.

Opening for Writing

writer, err := modelsdk.OpenForWriting("/path/to/MyApp.mpr")
if err != nil {
    panic(err)
}
defer writer.Close()

The writer provides access to the underlying reader via writer.Reader().

Complete Example

package main

import (
    "github.com/mendixlabs/mxcli"
)

func main() {
    // Open for writing
    writer, err := modelsdk.OpenForWriting("/path/to/MyApp.mpr")
    if err != nil {
        panic(err)
    }
    defer writer.Close()

    reader := writer.Reader()
    modules, _ := reader.ListModules()
    dm, _ := reader.GetDomainModel(modules[0].ID)

    // Create a new entity
    customer := modelsdk.NewEntity("Customer")
    writer.CreateEntity(dm.ID, customer)

    // Add attributes
    writer.AddAttribute(dm.ID, customer.ID, modelsdk.NewStringAttribute("Name", 200))
    writer.AddAttribute(dm.ID, customer.ID, modelsdk.NewStringAttribute("Email", 254))
    writer.AddAttribute(dm.ID, customer.ID, modelsdk.NewBooleanAttribute("IsActive"))
    writer.AddAttribute(dm.ID, customer.ID, modelsdk.NewDateTimeAttribute("CreatedDate", true))

    // Create another entity
    order := modelsdk.NewEntity("Order")
    writer.CreateEntity(dm.ID, order)

    // Create an association
    assoc := modelsdk.NewAssociation("Customer_Order", customer.ID, order.ID)
    writer.CreateAssociation(dm.ID, assoc)
}

Writer Methods

Modules

writer.CreateModule(module)
writer.UpdateModule(module)
writer.DeleteModule(id)

Entities

writer.CreateEntity(domainModelID, entity)
writer.UpdateEntity(domainModelID, entity)
writer.DeleteEntity(domainModelID, entityID)

Attributes

writer.AddAttribute(domainModelID, entityID, attribute)

Associations

writer.CreateAssociation(domainModelID, association)
writer.DeleteAssociation(domainModelID, associationID)

Microflows and Nanoflows

writer.CreateMicroflow(microflow)
writer.UpdateMicroflow(microflow)
writer.DeleteMicroflow(id)
writer.CreateNanoflow(nanoflow)
writer.UpdateNanoflow(nanoflow)
writer.DeleteNanoflow(id)

Pages and Layouts

writer.CreatePage(page)
writer.UpdatePage(page)
writer.DeletePage(id)
writer.CreateLayout(layout)
writer.UpdateLayout(layout)
writer.DeleteLayout(id)

Other

writer.CreateEnumeration(enumeration)
writer.CreateConstant(constant)

Helper Functions

// Create attributes
modelsdk.NewStringAttribute(name, length)
modelsdk.NewIntegerAttribute(name)
modelsdk.NewDecimalAttribute(name)
modelsdk.NewBooleanAttribute(name)
modelsdk.NewDateTimeAttribute(name, localize)
modelsdk.NewEnumerationAttribute(name, enumID)

// Create entities
modelsdk.NewEntity(name)                 // Persistable entity
modelsdk.NewNonPersistableEntity(name)   // Non-persistable entity

// Create associations
modelsdk.NewAssociation(name, parentID, childID)      // Reference (1:N)
modelsdk.NewReferenceSetAssociation(name, p, c)       // Reference set (M:N)

// Create flows
modelsdk.NewMicroflow(name)
modelsdk.NewNanoflow(name)

// Create pages
modelsdk.NewPage(name)

// Generate IDs
modelsdk.GenerateID()

Data Flow

sequenceDiagram
    participant App
    participant SDK as modelsdk
    participant Writer
    participant FS as File System

    App->>SDK: OpenForWriting("project.mpr")
    SDK->>Writer: NewWriter(path)
    Writer->>FS: Load existing units
    SDK-->>App: Writer

    App->>SDK: NewEntity()
    SDK-->>App: *Entity

    App->>SDK: AddEntity(dm, entity)
    SDK->>Writer: SerializeEntity(entity)
    Writer->>Writer: Generate BSON

    App->>SDK: Save()
    SDK->>Writer: Write all modified units
    Writer->>FS: Write .mxunit files
    Writer->>FS: Update .mpr metadata

Running the Example

cd examples/modify_project
go run main.go /path/to/MyApp.mpr

Warning: Always backup your .mpr file before modifying it.

Public API

The public API surface is defined in modelsdk.go at the package root. It provides convenience functions for opening projects and constructing model elements.

Entry Points

// Read-only access
reader, err := modelsdk.Open("/path/to/project.mpr")
defer reader.Close()

// Read-write access
writer, err := modelsdk.OpenForWriting("/path/to/project.mpr")
defer writer.Close()

Core Types

TypeDescription
modelsdk.IDUnique identifier for model elements (UUID)
modelsdk.ModuleRepresents a Mendix module
modelsdk.ProjectRepresents a Mendix project
modelsdk.DomainModelContains entities and associations
modelsdk.EntityAn entity in the domain model
modelsdk.AttributeAn attribute of an entity
modelsdk.AssociationA relationship between entities
modelsdk.MicroflowA microflow (server-side logic)
modelsdk.NanoflowA nanoflow (client-side logic)
modelsdk.PageA page in the UI
modelsdk.LayoutA page layout template

Package Structure

github.com/mendixlabs/mxcli/
├── modelsdk.go          # Main package with convenience functions
├── model/               # Core model types (ID, Module, Project, etc.)
├── api/                 # High-level fluent API (builders)
│   ├── api.go           # ModelAPI entry point
│   ├── domainmodels.go  # EntityBuilder, AssociationBuilder
│   ├── enumerations.go  # EnumerationBuilder
│   ├── microflows.go    # MicroflowBuilder
│   ├── pages.go         # PageBuilder, widget builders
│   └── modules.go       # ModulesAPI
├── sdk/
│   ├── domainmodel/     # Domain model types (Entity, Attribute, Association)
│   ├── microflows/      # Microflow and Nanoflow types
│   ├── pages/           # Page, Layout, and Widget types
│   └── mpr/             # MPR file reader and writer
└── examples/            # Example applications

Reader API

The reader provides read-only access to all project elements:

reader, _ := modelsdk.Open("path/to/project.mpr")
defer reader.Close()

// Metadata
reader.Path()                    // Get file path
reader.Version()                 // Get MPR version (1 or 2)
reader.GetMendixVersion()        // Get Mendix Studio Pro version

// Modules
reader.ListModules()             // List all modules
reader.GetModule(id)             // Get module by ID
reader.GetModuleByName(name)     // Get module by name

// Domain Models
reader.ListDomainModels()        // List all domain models
reader.GetDomainModel(moduleID)  // Get domain model for module

// Microflows & Nanoflows
reader.ListMicroflows()          // List all microflows
reader.GetMicroflow(id)          // Get microflow by ID
reader.ListNanoflows()           // List all nanoflows
reader.GetNanoflow(id)           // Get nanoflow by ID

// Pages & Layouts
reader.ListPages()               // List all pages
reader.GetPage(id)               // Get page by ID
reader.ListLayouts()             // List all layouts
reader.GetLayout(id)             // Get layout by ID

// Other
reader.ListEnumerations()        // List all enumerations
reader.ListConstants()           // List all constants
reader.ListScheduledEvents()     // List all scheduled events
reader.ExportJSON()              // Export entire model as JSON

Writer API

The writer provides read-write access. Use writer.Reader() for read operations:

writer, _ := modelsdk.OpenForWriting("path/to/project.mpr")
defer writer.Close()

reader := writer.Reader()

// Module operations
writer.CreateModule(module)
writer.UpdateModule(module)
writer.DeleteModule(id)

// Entity operations
writer.CreateEntity(domainModelID, entity)
writer.UpdateEntity(domainModelID, entity)
writer.DeleteEntity(domainModelID, entityID)

// Attribute operations
writer.AddAttribute(domainModelID, entityID, attribute)

// Association operations
writer.CreateAssociation(domainModelID, association)
writer.DeleteAssociation(domainModelID, associationID)

// Microflow operations
writer.CreateMicroflow(microflow)
writer.UpdateMicroflow(microflow)
writer.DeleteMicroflow(id)

// Page operations
writer.CreatePage(page)
writer.UpdatePage(page)
writer.DeletePage(id)

// Other
writer.CreateEnumeration(enumeration)
writer.CreateConstant(constant)

Constructor Functions

Helper functions for creating model elements with proper defaults:

// Attributes
modelsdk.NewStringAttribute(name, length)
modelsdk.NewIntegerAttribute(name)
modelsdk.NewDecimalAttribute(name)
modelsdk.NewBooleanAttribute(name)
modelsdk.NewDateTimeAttribute(name, localize)
modelsdk.NewEnumerationAttribute(name, enumID)

// Entities
modelsdk.NewEntity(name)                 // Persistable
modelsdk.NewNonPersistableEntity(name)   // Non-persistable

// Associations
modelsdk.NewAssociation(name, parentID, childID)      // 1:N
modelsdk.NewReferenceSetAssociation(name, p, c)       // M:N

// Flows
modelsdk.NewMicroflow(name)
modelsdk.NewNanoflow(name)

// Pages
modelsdk.NewPage(name)

// IDs
modelsdk.GenerateID()

Open / OpenForWriting

The two entry points for accessing a Mendix project from Go code.

Read-Only Access

Use modelsdk.Open() when you only need to inspect the project:

import modelsdk "github.com/mendixlabs/mxcli"

reader, err := modelsdk.Open("/path/to/MyApp.mpr")
if err != nil {
    log.Fatal(err)
}
defer reader.Close()

// Query the project
modules, _ := reader.ListModules()
dm, _ := reader.GetDomainModel(modules[0].ID)

The reader provides all query methods (listing modules, entities, microflows, pages, etc.) but cannot modify the project. The .mpr file is opened with a read-only SQLite connection.

Read-Write Access

Use modelsdk.OpenForWriting() when you need to create or modify elements:

import modelsdk "github.com/mendixlabs/mxcli"

writer, err := modelsdk.OpenForWriting("/path/to/MyApp.mpr")
if err != nil {
    log.Fatal(err)
}
defer writer.Close()

// Access read methods through the embedded reader
reader := writer.Reader()
modules, _ := reader.ListModules()

// Create new elements
entity := modelsdk.NewEntity("Customer")
writer.CreateEntity(domainModelID, entity)

The writer opens the .mpr file with a read-write SQLite connection. Always back up your .mpr file before modifying it – the library writes directly to the database.

Format Auto-Detection

Both functions automatically detect the MPR format version:

VersionMendixStorage
v1< 10.18Single .mpr SQLite file with BSON blobs
v2>= 10.18.mpr metadata + mprcontents/ folder with .mxunit files

No configuration is needed – the library inspects the file structure and selects the appropriate reader/writer implementation.

Error Handling

Both functions return an error if:

  • The file path does not exist
  • The file is not a valid SQLite database
  • The file is not a recognized MPR format
  • (For OpenForWriting) the file is read-only or locked by another process
reader, err := modelsdk.Open("/path/to/project.mpr")
if err != nil {
    // Handle: file not found, not a valid MPR, etc.
    log.Fatalf("Failed to open project: %v", err)
}

Pure Go – No CGO Required

The library uses modernc.org/sqlite, a pure Go SQLite implementation. No C compiler, CGO, or system SQLite library is needed. This simplifies cross-compilation and deployment.

Reader Methods

Complete list of methods available on the reader returned by modelsdk.Open().

Metadata

reader.Path()                    // string -- file path of the .mpr
reader.Version()                 // int -- MPR version (1 or 2)
reader.GetMendixVersion()        // string -- Mendix Studio Pro version (e.g., "11.6.0")

Modules

reader.ListModules()             // ([]*model.Module, error) -- all modules
reader.GetModule(id)             // (*model.Module, error) -- module by ID
reader.GetModuleByName(name)     // (*model.Module, error) -- module by name

Domain Models

reader.ListDomainModels()        // ([]*domainmodel.DomainModel, error) -- all domain models
reader.GetDomainModel(moduleID)  // (*domainmodel.DomainModel, error) -- domain model for a module

The DomainModel struct contains:

  • Entities – slice of *domainmodel.Entity, each with Attributes, Indexes, ValidationRules, AccessRules, and EventHandlers
  • Associations – slice of *domainmodel.Association

Microflows and Nanoflows

reader.ListMicroflows()          // ([]*microflows.Microflow, error) -- all microflows
reader.GetMicroflow(id)          // (*microflows.Microflow, error) -- microflow by ID
reader.ListNanoflows()           // ([]*microflows.Nanoflow, error) -- all nanoflows
reader.GetNanoflow(id)           // (*microflows.Nanoflow, error) -- nanoflow by ID

Pages and Layouts

reader.ListPages()               // ([]*pages.Page, error) -- all pages
reader.GetPage(id)               // (*pages.Page, error) -- page by ID
reader.ListLayouts()             // ([]*pages.Layout, error) -- all layouts
reader.GetLayout(id)             // (*pages.Layout, error) -- layout by ID

Enumerations

reader.ListEnumerations()        // ([]*model.Enumeration, error) -- all enumerations

Each enumeration contains:

  • Name – enumeration name
  • Values – slice of *model.EnumerationValue with Name and Caption

Other Documents

reader.ListConstants()           // ([]*model.Constant, error) -- all constants
reader.ListScheduledEvents()     // ([]*model.ScheduledEvent, error) -- all scheduled events

Export

reader.ExportJSON()              // ([]byte, error) -- entire model as JSON

Example: Full Project Scan

reader, _ := modelsdk.Open("/path/to/MyApp.mpr")
defer reader.Close()

fmt.Printf("Mendix version: %s\n", reader.GetMendixVersion())

modules, _ := reader.ListModules()
for _, m := range modules {
    fmt.Printf("Module: %s\n", m.Name)

    dm, _ := reader.GetDomainModel(m.ID)
    for _, entity := range dm.Entities {
        fmt.Printf("  Entity: %s (persistent: %v)\n", entity.Name, entity.Persistable)
        for _, attr := range entity.Attributes {
            fmt.Printf("    - %s: %s\n", attr.Name, attr.Type.GetTypeName())
        }
    }
}

microflows, _ := reader.ListMicroflows()
fmt.Printf("Total microflows: %d\n", len(microflows))

pages, _ := reader.ListPages()
fmt.Printf("Total pages: %d\n", len(pages))

Writer Methods

Complete list of methods available on the writer returned by modelsdk.OpenForWriting().

Accessing the Reader

The writer embeds a reader for querying the project:

writer, _ := modelsdk.OpenForWriting("/path/to/MyApp.mpr")
defer writer.Close()

reader := writer.Reader()
modules, _ := reader.ListModules()

Modules

writer.CreateModule(module)      // error -- create a new module
writer.UpdateModule(module)      // error -- update module metadata
writer.DeleteModule(id)          // error -- delete a module and its contents

Entities

writer.CreateEntity(domainModelID, entity)       // error -- add entity to domain model
writer.UpdateEntity(domainModelID, entity)       // error -- update an existing entity
writer.DeleteEntity(domainModelID, entityID)     // error -- remove entity from domain model

Attributes

writer.AddAttribute(domainModelID, entityID, attribute)  // error -- add attribute to entity

Associations

writer.CreateAssociation(domainModelID, association)     // error -- create association
writer.DeleteAssociation(domainModelID, associationID)   // error -- remove association

Microflows and Nanoflows

writer.CreateMicroflow(microflow)    // error -- create a new microflow
writer.UpdateMicroflow(microflow)    // error -- update an existing microflow
writer.DeleteMicroflow(id)           // error -- delete a microflow

writer.CreateNanoflow(nanoflow)      // error -- create a new nanoflow
writer.UpdateNanoflow(nanoflow)      // error -- update an existing nanoflow
writer.DeleteNanoflow(id)            // error -- delete a nanoflow

Pages and Layouts

writer.CreatePage(page)              // error -- create a new page
writer.UpdatePage(page)              // error -- update an existing page
writer.DeletePage(id)                // error -- delete a page

writer.CreateLayout(layout)          // error -- create a new layout
writer.UpdateLayout(layout)          // error -- update an existing layout
writer.DeleteLayout(id)              // error -- delete a layout

Enumerations and Constants

writer.CreateEnumeration(enumeration)    // error -- create a new enumeration
writer.CreateConstant(constant)          // error -- create a new constant

Helper Functions

The modelsdk package provides helper functions for creating common types:

// Entities
modelsdk.NewEntity(name)                          // *Entity -- persistent entity
modelsdk.NewNonPersistableEntity(name)            // *Entity -- non-persistent entity

// Attributes
modelsdk.NewStringAttribute(name, length)         // *Attribute
modelsdk.NewIntegerAttribute(name)                // *Attribute
modelsdk.NewDecimalAttribute(name)                // *Attribute
modelsdk.NewBooleanAttribute(name)                // *Attribute
modelsdk.NewDateTimeAttribute(name, localize)     // *Attribute
modelsdk.NewEnumerationAttribute(name, enumID)    // *Attribute
modelsdk.NewAutoNumberAttribute(name)             // *Attribute

// Associations
modelsdk.NewAssociation(name, parentID, childID)          // *Association -- Reference (1:N)
modelsdk.NewReferenceSetAssociation(name, parentID, childID)  // *Association -- ReferenceSet (M:N)

// Flows
modelsdk.NewMicroflow(name)     // *Microflow
modelsdk.NewNanoflow(name)      // *Nanoflow

// Pages
modelsdk.NewPage(name)          // *Page

// IDs
modelsdk.GenerateID()           // string -- new UUID v4

Example: Create an Entity with Attributes

writer, _ := modelsdk.OpenForWriting("/path/to/MyApp.mpr")
defer writer.Close()

reader := writer.Reader()
modules, _ := reader.ListModules()
dm, _ := reader.GetDomainModel(modules[0].ID)

// Create entity
customer := modelsdk.NewEntity("Customer")
customer.Documentation = "Customer master data"
customer.Location = model.Point{X: 100, Y: 200}
writer.CreateEntity(dm.ID, customer)

// Add attributes
writer.AddAttribute(dm.ID, customer.ID, modelsdk.NewStringAttribute("Name", 200))
writer.AddAttribute(dm.ID, customer.ID, modelsdk.NewStringAttribute("Email", 254))
writer.AddAttribute(dm.ID, customer.ID, modelsdk.NewBooleanAttribute("IsActive"))
writer.AddAttribute(dm.ID, customer.ID, modelsdk.NewDateTimeAttribute("CreatedDate", true))

// Create association
order := modelsdk.NewEntity("Order")
writer.CreateEntity(dm.ID, order)

assoc := modelsdk.NewAssociation("Customer_Order", customer.ID, order.ID)
writer.CreateAssociation(dm.ID, assoc)

Warning: Always back up your .mpr file before modifying it. The writer operates directly on the SQLite database.

Fluent API

The api/ package provides a high-level, fluent builder API inspired by the Mendix Web Extensibility Model API. It simplifies common operations with method chaining and sensible defaults.

Setup

package main

import (
    "github.com/mendixlabs/mxcli/api"
    "github.com/mendixlabs/mxcli/sdk/mpr"
)

func main() {
    writer, err := mpr.OpenForWriting("/path/to/MyApp.mpr")
    if err != nil {
        panic(err)
    }
    defer writer.Close()

    // Create the high-level API
    modelAPI := api.New(writer)

    // Set the current module context
    module, _ := modelAPI.Modules.GetModule("MyModule")
    modelAPI.SetModule(module)
}

API Structure

classDiagram
    class ModelAPI {
        +writer *Writer
        +reader *Reader
        +currentModule *Module
        +DomainModels *DomainModelsAPI
        +Microflows *MicroflowsAPI
        +Pages *PagesAPI
        +Enumerations *EnumerationsAPI
        +Modules *ModulesAPI
        +New(writer) *ModelAPI
        +SetModule(module) *ModelAPI
    }

    class EntityBuilder {
        +Persistent() *EntityBuilder
        +NonPersistent() *EntityBuilder
        +WithStringAttribute(name, length) *EntityBuilder
        +WithIntegerAttribute(name) *EntityBuilder
        +Build() (*Entity, error)
    }

    class MicroflowBuilder {
        +WithParameter(name, entity) *MicroflowBuilder
        +WithStringParameter(name) *MicroflowBuilder
        +ReturnsBoolean() *MicroflowBuilder
        +Build() (*Microflow, error)
    }

    ModelAPI --> EntityBuilder : creates
    ModelAPI --> MicroflowBuilder : creates

Namespaces

NamespaceDescription
modelAPI.DomainModelsCreate/modify entities, attributes, associations
modelAPI.EnumerationsCreate/modify enumerations and values
modelAPI.MicroflowsCreate microflows with parameters and return types
modelAPI.PagesCreate pages with widgets (DataView, TextBox, etc.)
modelAPI.ModulesList and retrieve modules

Creating Entities

// Create entity with fluent builder
customer, err := modelAPI.DomainModels.CreateEntity("Customer").
    Persistent().
    WithStringAttribute("Name", 100).
    WithStringAttribute("Email", 254).
    WithIntegerAttribute("Age").
    WithBooleanAttribute("IsActive").
    WithDateTimeAttribute("CreatedDate", true).
    Build()

Creating Associations

_, err := modelAPI.DomainModels.CreateAssociation("Customer_Orders").
    From("Customer").
    To("Order").
    OneToMany().
    Build()

Creating Enumerations

_, err := modelAPI.Enumerations.CreateEnumeration("OrderStatus").
    WithValue("Pending", "Pending").
    WithValue("Processing", "Processing").
    WithValue("Completed", "Completed").
    WithValue("Cancelled", "Cancelled").
    Build()

Creating Microflows

_, err := modelAPI.Microflows.CreateMicroflow("ACT_ProcessOrder").
    WithParameter("Order", "MyModule.Order").
    WithStringParameter("Message").
    ReturnsBoolean().
    Build()

Complete Example

package main

import (
    "github.com/mendixlabs/mxcli/api"
    "github.com/mendixlabs/mxcli/sdk/mpr"
)

func main() {
    writer, err := mpr.OpenForWriting("/path/to/MyApp.mpr")
    if err != nil {
        panic(err)
    }
    defer writer.Close()

    modelAPI := api.New(writer)

    module, _ := modelAPI.Modules.GetModule("MyModule")
    modelAPI.SetModule(module)

    // Create entity with fluent builder
    customer, _ := modelAPI.DomainModels.CreateEntity("Customer").
        Persistent().
        WithStringAttribute("Name", 100).
        WithStringAttribute("Email", 254).
        WithIntegerAttribute("Age").
        WithBooleanAttribute("IsActive").
        WithDateTimeAttribute("CreatedDate", true).
        Build()

    // Create another entity
    order, _ := modelAPI.DomainModels.CreateEntity("Order").
        Persistent().
        WithDecimalAttribute("TotalAmount").
        WithDateTimeAttribute("OrderDate", true).
        Build()

    // Create association between entities
    _, _ = modelAPI.DomainModels.CreateAssociation("Customer_Orders").
        From("Customer").
        To("Order").
        OneToMany().
        Build()

    // Create enumeration
    _, _ = modelAPI.Enumerations.CreateEnumeration("OrderStatus").
        WithValue("Pending", "Pending").
        WithValue("Processing", "Processing").
        WithValue("Completed", "Completed").
        WithValue("Cancelled", "Cancelled").
        Build()

    // Create microflow
    _, _ = modelAPI.Microflows.CreateMicroflow("ACT_ProcessOrder").
        WithParameter("Order", "MyModule.Order").
        WithStringParameter("Message").
        ReturnsBoolean().
        Build()
}

Package Files

FilePurpose
api/api.goModelAPI entry point with namespace access
api/domainmodels.goEntityBuilder, AssociationBuilder, AttributeBuilder
api/enumerations.goEnumerationBuilder, EnumValueBuilder
api/microflows.goMicroflowBuilder with parameters and return types
api/pages.goPageBuilder, widget builders
api/modules.goModulesAPI for module retrieval

ModelAPI Entry Point

The api package provides a high-level fluent API inspired by the Mendix Web Extensibility Model API. It wraps the lower-level reader/writer with builder patterns for common operations.

Creating the ModelAPI

import (
    "github.com/mendixlabs/mxcli/api"
    "github.com/mendixlabs/mxcli/sdk/mpr"
)

writer, _ := mpr.OpenForWriting("/path/to/MyApp.mpr")
defer writer.Close()

modelAPI := api.New(writer)

Setting the Module Context

Most operations require a module context. Set it before calling builders:

module, _ := modelAPI.Modules.GetModule("MyModule")
modelAPI.SetModule(module)

All subsequent Create* calls will create elements in this module.

Namespace Accessors

The ModelAPI struct provides access to domain-specific APIs through namespace accessors:

NamespaceAccessorDescription
DomainModelsmodelAPI.DomainModelsCreate/modify entities, attributes, associations
EnumerationsmodelAPI.EnumerationsCreate/modify enumerations and their values
MicroflowsmodelAPI.MicroflowsCreate microflows with parameters and return types
PagesmodelAPI.PagesCreate pages with widgets
ModulesmodelAPI.ModulesList and retrieve modules

DomainModels Namespace

// Create an entity
entity, _ := modelAPI.DomainModels.CreateEntity("Customer").
    Persistent().
    WithStringAttribute("Name", 200).
    Build()

// Create an association
assoc, _ := modelAPI.DomainModels.CreateAssociation("Customer_Orders").
    From("Customer").
    To("Order").
    OneToMany().
    Build()

Enumerations Namespace

enum, _ := modelAPI.Enumerations.CreateEnumeration("OrderStatus").
    WithValue("Draft", "Draft").
    WithValue("Active", "Active").
    WithValue("Closed", "Closed").
    Build()

Microflows Namespace

mf, _ := modelAPI.Microflows.CreateMicroflow("ACT_ProcessOrder").
    WithParameter("Order", "MyModule.Order").
    WithStringParameter("Note").
    ReturnsBoolean().
    Build()

Pages Namespace

page, _ := modelAPI.Pages.CreatePage("CustomerOverview").
    WithTitle("Customer Overview").
    Build()

Modules Namespace

// List all modules
modules, _ := modelAPI.Modules.ListModules()

// Get a specific module
module, _ := modelAPI.Modules.GetModule("MyModule")

MDL to Fluent API Mapping

MDL StatementFluent API Method
CREATE PERSISTENT ENTITYDomainModels.CreateEntity().Persistent().Build()
CREATE NON-PERSISTENT ENTITYDomainModels.CreateEntity().NonPersistent().Build()
CREATE ASSOCIATIONDomainModels.CreateAssociation().Build()
CREATE ENUMERATIONEnumerations.CreateEnumeration().Build()
CREATE MICROFLOWMicroflows.CreateMicroflow().Build()
CREATE PAGEPages.CreatePage().Build()

When to Use the Fluent API vs Direct SDK

Use CaseRecommended Approach
Simple CRUD operationsFluent API (less boilerplate)
Complex microflow constructionDirect SDK types (more control)
Bulk operationsDirect writer methods (batch efficiency)
Integration testingFluent API (readable test code)

Builders

The api package provides fluent builder types for constructing Mendix model elements. Each builder follows the pattern: create, configure with chained methods, then call Build().

EntityBuilder

Creates entities with attributes, persistence settings, and documentation.

entity, err := modelAPI.DomainModels.CreateEntity("Customer").
    Persistent().
    WithStringAttribute("Name", 200).
    WithStringAttribute("Email", 254).
    WithIntegerAttribute("Age").
    WithBooleanAttribute("IsActive").
    WithDateTimeAttribute("CreatedDate", true).
    WithDecimalAttribute("Revenue").
    WithEnumerationAttribute("Status", "MyModule.CustomerStatus").
    Build()

EntityBuilder Methods

MethodDescription
Persistent()Mark as persistent (stored in database)
NonPersistent()Mark as non-persistent (in-memory only)
WithStringAttribute(name, length)Add a string attribute
WithIntegerAttribute(name)Add an integer attribute
WithLongAttribute(name)Add a long attribute
WithDecimalAttribute(name)Add a decimal attribute
WithBooleanAttribute(name)Add a boolean attribute
WithDateTimeAttribute(name, localize)Add a datetime attribute
WithAutoNumberAttribute(name)Add an auto-number attribute
WithEnumerationAttribute(name, enumRef)Add an enumeration attribute
Build()Create the entity and write it to the project

AssociationBuilder

Creates associations (relationships) between entities.

assoc, err := modelAPI.DomainModels.CreateAssociation("Customer_Orders").
    From("Customer").
    To("Order").
    OneToMany().
    Build()

AssociationBuilder Methods

MethodDescription
From(entityName)Set the FROM entity (FK owner)
To(entityName)Set the TO entity (referenced)
OneToMany()Reference type with Default owner (1:N)
ManyToMany()ReferenceSet type (M:N)
Build()Create the association and write it to the project

EnumerationBuilder

Creates enumerations with named values and captions.

enum, err := modelAPI.Enumerations.CreateEnumeration("OrderStatus").
    WithValue("Draft", "Draft Order").
    WithValue("Pending", "Pending Approval").
    WithValue("Completed", "Completed").
    WithValue("Cancelled", "Cancelled").
    Build()

EnumerationBuilder Methods

MethodDescription
WithValue(name, caption)Add a value with its display caption
Build()Create the enumeration and write it to the project

MicroflowBuilder

Creates microflows with parameters and return types.

mf, err := modelAPI.Microflows.CreateMicroflow("ACT_ProcessOrder").
    WithParameter("Order", "Sales.Order").
    WithStringParameter("Message").
    ReturnsBoolean().
    Build()

MicroflowBuilder Methods

MethodDescription
WithParameter(name, entityType)Add an object parameter
WithStringParameter(name)Add a string parameter
ReturnsBoolean()Set return type to Boolean
ReturnsString()Set return type to String
ReturnsVoid()Set return type to Nothing (void)
Build()Create the microflow and write it to the project

PageBuilder

Creates pages with title and layout configuration.

page, err := modelAPI.Pages.CreatePage("CustomerOverview").
    WithTitle("Customer Overview").
    Build()

PageBuilder Methods

MethodDescription
WithTitle(title)Set the page title
Build()Create the page and write it to the project

Error Handling

All Build() methods return (result, error). Common errors include:

  • Module context not set (modelAPI.SetModule() not called)
  • Entity not found (for associations referencing nonexistent entities)
  • Duplicate names within the same module
  • Invalid attribute types or references
entity, err := modelAPI.DomainModels.CreateEntity("Customer").
    Persistent().
    Build()
if err != nil {
    log.Fatalf("Failed to create entity: %v", err)
}

Examples

Complete fluent API examples for common tasks.

Setup

All examples start with the same setup:

package main

import (
    "log"
    "github.com/mendixlabs/mxcli/api"
    "github.com/mendixlabs/mxcli/sdk/mpr"
)

func main() {
    writer, err := mpr.OpenForWriting("/path/to/MyApp.mpr")
    if err != nil {
        log.Fatal(err)
    }
    defer writer.Close()

    modelAPI := api.New(writer)
    module, _ := modelAPI.Modules.GetModule("Sales")
    modelAPI.SetModule(module)

    // ... examples below
}

Create a Domain Model

// Create entity with multiple attribute types
customer, _ := modelAPI.DomainModels.CreateEntity("Customer").
    Persistent().
    WithStringAttribute("Name", 200).
    WithStringAttribute("Email", 254).
    WithIntegerAttribute("Age").
    WithBooleanAttribute("IsActive").
    WithDateTimeAttribute("CreatedDate", true).
    Build()

// Create another entity
order, _ := modelAPI.DomainModels.CreateEntity("Order").
    Persistent().
    WithDecimalAttribute("TotalAmount").
    WithDateTimeAttribute("OrderDate", true).
    Build()

// Create an enumeration
_, _ = modelAPI.Enumerations.CreateEnumeration("OrderStatus").
    WithValue("Pending", "Pending").
    WithValue("Processing", "Processing").
    WithValue("Completed", "Completed").
    WithValue("Cancelled", "Cancelled").
    Build()

// Create association
_, _ = modelAPI.DomainModels.CreateAssociation("Customer_Orders").
    From("Customer").
    To("Order").
    OneToMany().
    Build()

Create a Microflow

_, _ = modelAPI.Microflows.CreateMicroflow("ACT_ProcessOrder").
    WithParameter("Order", "Sales.Order").
    WithStringParameter("Message").
    ReturnsBoolean().
    Build()

Create a Page

_, _ = modelAPI.Pages.CreatePage("CustomerOverview").
    WithTitle("Customer Overview").
    Build()

Complete Module Setup

This example creates a full module with entities, enumerations, associations, and microflows:

package main

import (
    "log"
    "github.com/mendixlabs/mxcli/api"
    "github.com/mendixlabs/mxcli/sdk/mpr"
)

func main() {
    writer, err := mpr.OpenForWriting("/path/to/MyApp.mpr")
    if err != nil {
        log.Fatal(err)
    }
    defer writer.Close()

    modelAPI := api.New(writer)
    module, _ := modelAPI.Modules.GetModule("ProductCatalog")
    modelAPI.SetModule(module)

    // Enumeration
    _, _ = modelAPI.Enumerations.CreateEnumeration("ProductCategory").
        WithValue("Electronics", "Electronics").
        WithValue("Clothing", "Clothing").
        WithValue("HomeGarden", "Home & Garden").
        Build()

    // Entities
    _, _ = modelAPI.DomainModels.CreateEntity("Product").
        Persistent().
        WithStringAttribute("SKU", 50).
        WithStringAttribute("Name", 200).
        WithDecimalAttribute("Price").
        WithIntegerAttribute("StockQuantity").
        WithBooleanAttribute("IsActive").
        WithEnumerationAttribute("Category", "ProductCatalog.ProductCategory").
        Build()

    _, _ = modelAPI.DomainModels.CreateEntity("Supplier").
        Persistent().
        WithStringAttribute("CompanyName", 200).
        WithStringAttribute("ContactEmail", 254).
        Build()

    // Association
    _, _ = modelAPI.DomainModels.CreateAssociation("Product_Supplier").
        From("Product").
        To("Supplier").
        OneToMany().
        Build()

    // Microflow
    _, _ = modelAPI.Microflows.CreateMicroflow("ValidateProduct").
        WithParameter("Product", "ProductCatalog.Product").
        ReturnsBoolean().
        Build()
}

Comparison: Fluent API vs Direct SDK

The fluent API reduces boilerplate. Here is the same entity creation using both approaches:

Fluent API

entity, _ := modelAPI.DomainModels.CreateEntity("Customer").
    Persistent().
    WithStringAttribute("Name", 200).
    WithIntegerAttribute("Age").
    Build()

Direct SDK

import (
    "github.com/mendixlabs/mxcli/model"
    "github.com/mendixlabs/mxcli/sdk/domainmodel"
    modelsdk "github.com/mendixlabs/mxcli"
)

entity := &domainmodel.Entity{
    BaseElement: model.BaseElement{
        ID: model.ID(modelsdk.GenerateID()),
    },
    Name:        "Customer",
    Persistable: true,
    Attributes: []*domainmodel.Attribute{
        {
            BaseElement: model.BaseElement{ID: model.ID(modelsdk.GenerateID())},
            Name:        "Name",
            Type:        &domainmodel.StringAttributeType{Length: 200},
        },
        {
            BaseElement: model.BaseElement{ID: model.ID(modelsdk.GenerateID())},
            Name:        "Age",
            Type:        &domainmodel.IntegerAttributeType{},
        },
    },
}

err := writer.CreateEntity(domainModelID, entity)

The fluent API handles ID generation, domain model lookup, and module context automatically.

Connection Statements

Statements for opening and closing Mendix project files.

StatementDescription
OPEN PROJECTOpen a Mendix project file for reading or modification
CLOSE PROJECTClose the currently open project and release the file handle

OPEN PROJECT

Synopsis

CONNECT LOCAL 'path/to/app.mpr'

Description

Opens a Mendix project file (.mpr) for reading and modification. The path can be absolute or relative to the current working directory. Once connected, all query and mutation statements operate against this project.

MDL supports both MPR v1 (single .mpr SQLite file, Mendix < 10.18) and MPR v2 (.mpr metadata + mprcontents/ folder, Mendix >= 10.18). The format is detected automatically.

Only one project can be open at a time. If a project is already open, close it with DISCONNECT before opening another.

Parameters

path
Path to the .mpr file. Can be an absolute path or a path relative to the current working directory. The path must be enclosed in single quotes.

Examples

Open a project with an absolute path

CONNECT LOCAL '/Users/dev/projects/MyApp/MyApp.mpr';

Open a project with a relative path

CONNECT LOCAL './mx-test-projects/test1-go-app/test1-go.mpr';

Open via the CLI flag

Instead of using CONNECT LOCAL inside the REPL, you can specify the project path when launching the CLI:

-- These are shell commands, not MDL:
-- mxcli -p /path/to/app.mpr
-- mxcli -p app.mpr -c "SHOW ENTITIES"

See Also

CLOSE PROJECT, SHOW STATUS

CLOSE PROJECT

Synopsis

DISCONNECT

Description

Closes the currently open project and releases the file handle. Any uncommitted changes are discarded. After disconnecting, query and mutation statements are unavailable until a new project is opened with CONNECT LOCAL.

Parameters

This statement takes no parameters.

Examples

Close the current project

DISCONNECT;

Open a different project

DISCONNECT;
CONNECT LOCAL '/path/to/other-project.mpr';

See Also

OPEN PROJECT, SHOW STATUS

Query Statements

Statements for browsing and inspecting project elements. Query statements are read-only and never modify the project.

SHOW Statements

StatementDescription
SHOW MODULESList all modules in the project
SHOW ENTITIESList entities, optionally filtered by module
SHOW ASSOCIATIONSList associations, optionally filtered by module
SHOW ENUMERATIONSList enumerations, optionally filtered by module
SHOW MICROFLOWSList microflows or nanoflows
SHOW PAGESList pages or snippets
SHOW CONSTANTSList constants, optionally filtered by module
SHOW WORKFLOWSList workflows, optionally filtered by module
SHOW BUSINESS EVENTSList business event services
SHOW STRUCTUREHierarchical project overview at configurable depth
SHOW WIDGETSList widgets across pages with optional filtering

DESCRIBE Statements

StatementDescription
DESCRIBE ENTITYShow complete MDL source for an entity
DESCRIBE ASSOCIATIONShow association details
DESCRIBE ENUMERATIONShow enumeration values and documentation
DESCRIBE MICROFLOWShow complete MDL source for a microflow or nanoflow
DESCRIBE PAGEShow complete MDL source for a page or snippet

Search

StatementDescription
SEARCHFull-text search across all project strings and source

SHOW MODULES

Synopsis

SHOW MODULES

Description

Lists all modules in the current project with their names. This is typically the first command used after connecting to a project to understand its structure.

Module names returned by this command are used as the IN <module> filter for other SHOW and DESCRIBE statements.

Parameters

This statement takes no parameters.

Examples

List all modules in the project:

SHOW MODULES

Use the result to explore a specific module:

SHOW MODULES
SHOW ENTITIES IN MyFirstModule

See Also

SHOW STRUCTURE, SHOW ENTITIES, SHOW MICROFLOWS, SHOW PAGES

SHOW ENTITIES

Synopsis

SHOW ENTITIES [IN <module>]

SHOW ENTITY <qualified_name>

Description

Lists entities in the project. Without the IN clause, lists all entities across all modules. With IN <module>, restricts the listing to entities in the specified module.

The SHOW ENTITY variant displays a summary of a single entity by its qualified name, including its type (persistent/non-persistent) and attribute count.

Parameters

module
The name of the module to filter by. Only entities belonging to this module are shown.
qualified_name
A Module.EntityName reference identifying a specific entity.

Examples

List all entities in the project:

SHOW ENTITIES

List entities in a specific module:

SHOW ENTITIES IN Sales

Show summary of a single entity:

SHOW ENTITY Sales.Customer

See Also

DESCRIBE ENTITY, SHOW ASSOCIATIONS, SHOW MODULES

SHOW MICROFLOWS / SHOW NANOFLOWS

Synopsis

SHOW MICROFLOWS [IN <module>]

SHOW NANOFLOWS [IN <module>]

Description

Lists microflows or nanoflows in the project. Without the IN clause, lists all microflows (or nanoflows) across all modules. With IN <module>, restricts the listing to the specified module.

Microflows run on the server side and can perform database operations, call external services, and execute Java actions. Nanoflows run on the client side and are used for offline-capable and low-latency logic.

Parameters

module
The name of the module to filter by. Only microflows or nanoflows belonging to this module are shown.

Examples

List all microflows in the project:

SHOW MICROFLOWS

List microflows in a specific module:

SHOW MICROFLOWS IN Administration

List all nanoflows:

SHOW NANOFLOWS

List nanoflows in a specific module:

SHOW NANOFLOWS IN MyFirstModule

See Also

DESCRIBE MICROFLOW, SHOW PAGES, SHOW MODULES

SHOW PAGES / SHOW SNIPPETS

Synopsis

SHOW PAGES [IN <module>]

SHOW SNIPPETS [IN <module>]

Description

Lists pages or snippets in the project. Without the IN clause, lists all pages (or snippets) across all modules. With IN <module>, restricts the listing to the specified module.

Pages are the user interface screens of a Mendix application. Snippets are reusable page fragments that can be embedded in multiple pages via SNIPPETCALL widgets.

Parameters

module
The name of the module to filter by. Only pages or snippets belonging to this module are shown.

Examples

List all pages in the project:

SHOW PAGES

List pages in a specific module:

SHOW PAGES IN Sales

List all snippets:

SHOW SNIPPETS

List snippets in a specific module:

SHOW SNIPPETS IN Common

See Also

DESCRIBE PAGE, SHOW WIDGETS, SHOW MODULES

SHOW ENUMERATIONS

Synopsis

SHOW ENUMERATIONS [IN <module>]

Description

Lists enumerations in the project. Without the IN clause, lists all enumerations across all modules. With IN <module>, restricts the listing to enumerations in the specified module.

Enumerations define a fixed set of named values that can be used as attribute types on entities.

Parameters

module
The name of the module to filter by. Only enumerations belonging to this module are shown.

Examples

List all enumerations in the project:

SHOW ENUMERATIONS

List enumerations in a specific module:

SHOW ENUMERATIONS IN Sales

See Also

DESCRIBE ENUMERATION, SHOW ENTITIES, SHOW MODULES

SHOW ASSOCIATIONS

Synopsis

SHOW ASSOCIATIONS [IN <module>]

SHOW ASSOCIATION <qualified_name>

Description

Lists associations in the project. Without the IN clause, lists all associations across all modules. With IN <module>, restricts the listing to associations in the specified module.

The SHOW ASSOCIATION variant displays a summary of a single association by its qualified name, including the FROM entity, TO entity, and association type.

Parameters

module
The name of the module to filter by. Only associations belonging to this module are shown.
qualified_name
A Module.AssociationName reference identifying a specific association.

Examples

List all associations in the project:

SHOW ASSOCIATIONS

List associations in a specific module:

SHOW ASSOCIATIONS IN Sales

Show a single association:

SHOW ASSOCIATION Sales.Order_Customer

See Also

DESCRIBE ASSOCIATION, SHOW ENTITIES, SHOW MODULES

SHOW CONSTANTS

Synopsis

SHOW CONSTANTS [IN <module>]

Description

Lists constants defined in the project. Without the IN clause, lists all constants across all modules. With IN <module>, restricts the listing to constants in the specified module.

Constants are named values (strings, integers, booleans, etc.) that can be configured per deployment environment. They are typically used for API URLs, feature flags, and configuration values.

Parameters

module
The name of the module to filter by. Only constants belonging to this module are shown.

Examples

List all constants in the project:

SHOW CONSTANTS

List constants in a specific module:

SHOW CONSTANTS IN MyModule

See Also

SHOW MODULES, SHOW STRUCTURE

SHOW WORKFLOWS

Synopsis

SHOW WORKFLOWS [IN <module>]

Description

Lists workflows defined in the project. Without the IN clause, lists all workflows across all modules. With IN <module>, restricts the listing to workflows in the specified module.

Workflows model multi-step business processes with user tasks, decisions, parallel paths, and microflow calls.

Parameters

module
The name of the module to filter by. Only workflows belonging to this module are shown.

Examples

List all workflows in the project:

SHOW WORKFLOWS

List workflows in a specific module:

SHOW WORKFLOWS IN Approvals

See Also

SHOW MODULES, SHOW MICROFLOWS, SHOW STRUCTURE

SHOW BUSINESS EVENTS

Synopsis

SHOW BUSINESS EVENTS [IN <module>]

Description

Lists business event services defined in the project. Without the IN clause, lists all business event services across all modules. With IN <module>, restricts the listing to services in the specified module.

Business event services enable asynchronous, event-driven communication between Mendix applications using the Mendix Business Events platform.

Parameters

module
The name of the module to filter by. Only business event services belonging to this module are shown.

Examples

List all business event services:

SHOW BUSINESS EVENTS

List business event services in a specific module:

SHOW BUSINESS EVENTS IN OrderModule

See Also

SHOW MODULES, SHOW STRUCTURE

SHOW IMAGE COLLECTION

Synopsis

SHOW IMAGE COLLECTION;
SHOW IMAGE COLLECTION IN module;

Description

Lists all image collections in the project. Use IN module to filter to a specific module. For full details including embedded images, use DESCRIBE IMAGE COLLECTION.

Parameters

IN module
Optional. Restricts the listing to collections in the specified module.

Examples

List all collections

SHOW IMAGE COLLECTION;

Filter by module

SHOW IMAGE COLLECTION IN MyModule;

Describe a specific collection

DESCRIBE IMAGE COLLECTION MyModule.AppIcons;

See Also

DESCRIBE IMAGE COLLECTION, CREATE IMAGE COLLECTION, SHOW MODULES

SHOW STRUCTURE

Synopsis

SHOW STRUCTURE [DEPTH 1|2|3] [IN <module>] [ALL]

Description

Shows a hierarchical overview of the project structure. The output depth and scope can be controlled with optional clauses. By default, displays depth 2 (documents with signatures) for user modules only, excluding system and marketplace modules.

This is useful for getting a quick birds-eye view of what a project contains without examining each element individually.

Parameters

DEPTH
Controls the level of detail in the output. 1 shows one line per module with element counts. 2 (default) shows documents with their signatures. 3 shows full detail including typed attributes and named parameters.
module
When IN <module> is specified, restricts output to a single module. Implicitly uses depth 2 unless overridden.
ALL
Include system and marketplace modules in the output. Without this flag, only user-created modules are shown.

Examples

Show module-level summary (one line per module):

SHOW STRUCTURE DEPTH 1

Show default structure (documents with signatures, user modules only):

SHOW STRUCTURE

Show structure of a single module:

SHOW STRUCTURE IN MyFirstModule

Show full detail including all modules:

SHOW STRUCTURE DEPTH 3 ALL

See Also

SHOW MODULES, SHOW ENTITIES, SHOW MICROFLOWS, SHOW PAGES

SHOW WIDGETS

Synopsis

SHOW WIDGETS [IN <module>] [WHERE <condition>]

Description

Lists widgets used across pages in the project. This command queries the catalog and requires REFRESH CATALOG FULL to have been run first. Without filters, lists all widgets. The IN clause restricts results to a specific module, and the WHERE clause enables filtering on widget properties.

This is useful for auditing widget usage, finding pages that use a particular widget type, or planning bulk widget updates.

Parameters

module
The name of the module to filter by. Only widgets on pages belonging to this module are shown.
condition
A filter expression on widget properties. Supported filter columns include WidgetType, PageName, ModuleName, and other catalog fields.

Examples

List all widgets (requires catalog):

REFRESH CATALOG FULL
SHOW WIDGETS

List widgets in a specific module:

SHOW WIDGETS IN Sales

Filter widgets by type:

SHOW WIDGETS WHERE WidgetType = 'DataGrid'

See Also

SHOW PAGES, DESCRIBE PAGE, SHOW STRUCTURE

DESCRIBE ENTITY

Synopsis

DESCRIBE ENTITY <qualified_name>

Description

Shows the complete MDL source for an entity, including its type (persistent, non-persistent, view, external), all attributes with types and constraints, indexes, access rules, and documentation comments. The output is round-trippable MDL that can be used directly in CREATE ENTITY statements.

This is the primary way to inspect the full definition of an entity before modifying it with ALTER ENTITY or recreating it with CREATE OR MODIFY.

Parameters

qualified_name
A Module.EntityName reference identifying the entity to describe. Both the module and entity name are required.

Examples

Describe a customer entity:

DESCRIBE ENTITY Sales.Customer

Example output:

/**
 * Customer entity stores customer information.
 */
@Position(100, 200)
CREATE PERSISTENT ENTITY Sales.Customer (
  /** Customer identifier */
  CustomerId: AutoNumber NOT NULL UNIQUE DEFAULT 1,
  Name: String(200) NOT NULL,
  Email: String(200),
  Status: Enumeration(Sales.CustomerStatus) DEFAULT 'Active'
)
INDEX (Name);

Describe a non-persistent entity:

DESCRIBE ENTITY Sales.CustomerFilter

See Also

SHOW ENTITIES, DESCRIBE ASSOCIATION, DESCRIBE ENUMERATION

DESCRIBE MICROFLOW / DESCRIBE NANOFLOW

Synopsis

DESCRIBE MICROFLOW <qualified_name>

DESCRIBE NANOFLOW <qualified_name>

Description

Shows the complete MDL source for a microflow or nanoflow, including parameters, return type, variable declarations, activities, control flow (IF/LOOP), error handling, and annotations. The output is round-trippable MDL that can be used directly in CREATE MICROFLOW or CREATE NANOFLOW statements.

This is useful for understanding existing logic before modifying it, or for extracting a microflow definition to use as a template.

Parameters

qualified_name
A Module.MicroflowName or Module.NanoflowName reference identifying the flow to describe. Both the module and flow name are required.

Examples

Describe a microflow:

DESCRIBE MICROFLOW Sales.ACT_CreateOrder

Example output:

CREATE MICROFLOW Sales.ACT_CreateOrder
FOLDER 'Orders'
BEGIN
  DECLARE $Order Sales.Order;
  $Order = CREATE Sales.Order (
    OrderDate = [%CurrentDateTime%],
    Status = 'Draft'
  );
  COMMIT $Order;
  SHOW PAGE Sales.Order_Edit ($Order = $Order);
  RETURN $Order;
END;

Describe a nanoflow:

DESCRIBE NANOFLOW MyModule.NAV_ValidateInput

See Also

SHOW MICROFLOWS, DESCRIBE PAGE, DESCRIBE ENTITY

DESCRIBE PAGE / DESCRIBE SNIPPET

Synopsis

DESCRIBE PAGE <qualified_name>

DESCRIBE SNIPPET <qualified_name>

Description

Shows the complete MDL source for a page or snippet, including page properties (title, layout, parameters), the full widget tree with all property values, and nested widget structures. The output is round-trippable MDL that can be used directly in CREATE PAGE or CREATE SNIPPET statements.

This is particularly useful before using ALTER PAGE or ALTER SNIPPET, as it reveals the widget names needed for targeted modifications.

Parameters

qualified_name
A Module.PageName or Module.SnippetName reference identifying the page or snippet to describe. Both the module and document name are required.

Examples

Describe a page:

DESCRIBE PAGE Sales.Customer_Edit

Example output:

CREATE PAGE Sales.Customer_Edit
(
  Params: { $Customer: Sales.Customer },
  Title: 'Edit Customer',
  Layout: Atlas_Core.PopupLayout
)
{
  DATAVIEW dvCustomer (DataSource: $Customer) {
    TEXTBOX txtName (Label: 'Name', Attribute: Name)
    TEXTBOX txtEmail (Label: 'Email', Attribute: Email)
    FOOTER footer1 {
      ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)
      ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES)
    }
  }
}

Describe a snippet:

DESCRIBE SNIPPET Common.NavigationMenu

See Also

SHOW PAGES, SHOW WIDGETS, DESCRIBE MICROFLOW

DESCRIBE ENUMERATION

Synopsis

DESCRIBE ENUMERATION <qualified_name>

Description

Shows the complete MDL source for an enumeration, including all values with their captions and any documentation comments. The output is round-trippable MDL that can be used directly in a CREATE ENUMERATION statement.

Enumerations define a fixed set of named values used as attribute types on entities.

Parameters

qualified_name
A Module.EnumerationName reference identifying the enumeration to describe. Both the module and enumeration name are required.

Examples

Describe an enumeration:

DESCRIBE ENUMERATION Sales.OrderStatus

Example output:

/** Order status enumeration */
CREATE ENUMERATION Sales.OrderStatus (
  Draft 'Draft',
  Pending 'Pending Approval',
  Approved 'Approved',
  Shipped 'Shipped',
  Delivered 'Delivered',
  Cancelled 'Cancelled'
);

See Also

SHOW ENUMERATIONS, DESCRIBE ENTITY, SHOW MODULES

DESCRIBE ASSOCIATION

Synopsis

DESCRIBE ASSOCIATION <qualified_name>

Description

Shows the complete MDL source for an association, including the FROM entity, TO entity, association type (Reference or ReferenceSet), owner, and delete behavior. The output is round-trippable MDL that can be used directly in a CREATE ASSOCIATION statement.

Associations define relationships between entities. A Reference type is a one-to-many relationship, while ReferenceSet is many-to-many.

Parameters

qualified_name
A Module.AssociationName reference identifying the association to describe. Both the module and association name are required.

Examples

Describe an association:

DESCRIBE ASSOCIATION Sales.Order_Customer

Example output:

/** Links orders to customers */
CREATE ASSOCIATION Sales.Order_Customer
  FROM Sales.Customer
  TO Sales.Order
  TYPE Reference
  OWNER Default
  DELETE_BEHAVIOR DELETE_BUT_KEEP_REFERENCES;

Describe a many-to-many association:

DESCRIBE ASSOCIATION Sales.Order_Product

See Also

SHOW ASSOCIATIONS, DESCRIBE ENTITY, SHOW MODULES

SEARCH

Synopsis

SEARCH '<keyword>'

Description

Performs a full-text search across all strings, messages, captions, and MDL source in the project. Uses FTS5 (SQLite full-text search) for fast matching. Returns results grouped by document type and location.

This command requires the catalog to be populated. If search returns no results, run REFRESH CATALOG FULL first. The search is also available as a CLI subcommand: mxcli search -p app.mpr "keyword" --format names|json.

Parameters

keyword
The search term to look for. Must be enclosed in single quotes. Matches against entity names, attribute names, microflow activity captions, page titles, widget labels, string constants, log messages, and MDL source text.

Examples

Search for a keyword:

SEARCH 'Customer'

Search for an error message:

SEARCH 'is required'

Search using the CLI:

-- From the command line:
-- mxcli search -p app.mpr "Customer" --format names
-- mxcli search -p app.mpr "Customer" --format json

See Also

SHOW STRUCTURE, SHOW ENTITIES, SHOW MICROFLOWS

Domain Model Statements

Statements for creating, altering, and dropping domain model elements: entities, enumerations, associations, and constants.

Entity Statements

StatementDescription
CREATE ENTITYCreate a new entity in a module’s domain model
ALTER ENTITYAdd, drop, modify, or rename attributes and indexes on an existing entity
DROP ENTITYRemove an entity from the domain model

Enumeration Statements

StatementDescription
CREATE ENUMERATIONCreate a new enumeration type with named values
DROP ENUMERATIONRemove an enumeration from the project

Association Statements

StatementDescription
CREATE ASSOCIATIONCreate a relationship between two entities
DROP ASSOCIATIONRemove an association from the domain model

Constant Statements

StatementDescription
CREATE CONSTANTCreate a named constant value in a module

CREATE ENTITY

Synopsis

CREATE [ OR MODIFY ] entity_type ENTITY module.name
    [ EXTENDS parent_module.parent_entity ]
(
    attribute_name : data_type [ NOT NULL [ ERROR 'message' ] ]
                               [ UNIQUE [ ERROR 'message' ] ]
                               [ DEFAULT value ]
                               [ CALCULATED [ BY module.microflow ] ]
    [, ...]
)
[ INDEX ( column [ ASC | DESC ] [, ...] ) ]
[, ...]

CREATE VIEW ENTITY module.name (
    attribute_name : data_type [, ...]
) AS
    oql_query

Where entity_type is one of:

PERSISTENT
NON-PERSISTENT
EXTERNAL

Description

CREATE ENTITY adds a new entity to a module’s domain model. The entity type determines how instances are stored:

  • PERSISTENT entities are backed by a database table. This is the most common type and is required for data that must survive a server restart.
  • NON-PERSISTENT entities exist only in memory for the duration of a user session. They are commonly used for search filters, wizard state, and transient view models.
  • VIEW entities are read-only entities backed by an OQL query. They appear in the domain model but do not have their own database table.
  • EXTERNAL entities are sourced from an external service (e.g., OData). See CREATE EXTERNAL ENTITY for the full syntax.

If OR MODIFY is specified, the statement is idempotent: if an entity with the same qualified name already exists, its attributes are updated to match the definition. New attributes are added, and existing attributes have their types and constraints updated. This is useful for repeatable scripts.

The optional EXTENDS clause establishes generalization (inheritance). The child entity inherits all attributes from the parent and can add its own. Common parent entities include System.User, System.FileDocument, and System.Image. The EXTENDS clause must appear before the opening parenthesis.

Each attribute definition specifies a name, a data type, and optional constraints. Attributes are separated by commas. The supported data types are:

TypeSyntaxNotes
StringString(length)Length in characters; use String(unlimited) for unbounded
IntegerInteger32-bit signed integer
LongLong64-bit signed integer
DecimalDecimalArbitrary-precision decimal
BooleanBooleanTRUE or FALSE; defaults to FALSE when no DEFAULT is given
DateTimeDateTimeDate and time combined
DateDateDate only
AutoNumberAutoNumberAuto-incrementing integer (persistent entities only)
BinaryBinaryBinary data
HashedStringHashedStringOne-way hashed string (for passwords)
EnumerationEnumeration(Module.EnumName)Reference to an enumeration type

Documentation comments (/** ... */) placed immediately before the entity or an individual attribute are preserved as documentation in the domain model.

The @Position(x, y) annotation controls the entity’s visual position in the domain model diagram in Mendix Studio Pro.

One or more INDEX clauses may follow the attribute list to create database indexes. Each index lists one or more columns with an optional sort direction (ASC or DESC, defaulting to ASC).

Parameters

OR MODIFY
Makes the statement idempotent. If the entity already exists, its definition is updated to match. New attributes are added and existing attributes are modified. Without this clause, creating a duplicate entity is an error.
entity_type
One of PERSISTENT, NON-PERSISTENT, VIEW, or EXTERNAL. Determines how entity instances are stored. PERSISTENT is the most common choice.
module.name
The qualified name of the entity in the form Module.EntityName. The module must already exist.
EXTENDS parent_module.parent_entity
Establishes generalization (inheritance). The new entity inherits all attributes from the parent. Must appear before the opening parenthesis.
attribute_name
The name of an attribute. Must be a valid identifier and unique within the entity.
data_type
The attribute’s data type. See the table above for all supported types. String requires an explicit length argument.
NOT NULL
Marks the attribute as required. An optional ERROR 'message' clause provides a custom validation message shown when the constraint is violated.
UNIQUE
Marks the attribute as having a uniqueness constraint. An optional ERROR 'message' clause provides a custom validation message.
DEFAULT value
Sets the default value assigned to the attribute when a new object is created. String defaults are quoted ('value'), numeric defaults are bare (0, 3.14), and boolean defaults are TRUE or FALSE.
CALCULATED
Marks the attribute as calculated (not stored in the database). The attribute’s value is derived at runtime. Use CALCULATED BY Module.Microflow to specify the microflow that computes the value.
INDEX ( column [, …] )
Creates a database index on the listed columns. Each column may have an optional ASC or DESC sort direction. Multiple INDEX clauses create multiple indexes.

Examples

Basic persistent entity

CREATE PERSISTENT ENTITY Sales.Product (
    Name: String(200) NOT NULL,
    Price: Decimal DEFAULT 0,
    InStock: Boolean DEFAULT TRUE
);

Entity with all constraint types

/** Customer master data */
@Position(100, 200)
CREATE PERSISTENT ENTITY Sales.Customer (
    /** Unique customer identifier */
    CustomerId: AutoNumber NOT NULL UNIQUE DEFAULT 1,

    /** Customer full name */
    Name: String(200) NOT NULL ERROR 'Name is required',

    Email: String(200) UNIQUE ERROR 'Email must be unique',

    Balance: Decimal DEFAULT 0,

    IsActive: Boolean DEFAULT TRUE,

    CreatedDate: DateTime,

    Status: Enumeration(Sales.CustomerStatus) DEFAULT 'Active'
)
INDEX (Name)
INDEX (Email);

Entity with generalization (EXTENDS)

/** Employee extends the system user entity */
CREATE PERSISTENT ENTITY HR.Employee EXTENDS System.User (
    EmployeeNumber: String(20) NOT NULL UNIQUE,
    Department: String(100),
    HireDate: Date
);

File and image entities

CREATE PERSISTENT ENTITY Catalog.ProductPhoto EXTENDS System.Image (
    Caption: String(200),
    SortOrder: Integer DEFAULT 0
);

CREATE PERSISTENT ENTITY Docs.Attachment EXTENDS System.FileDocument (
    Description: String(500)
);

Non-persistent entity (search filter)

CREATE NON-PERSISTENT ENTITY Sales.CustomerFilter (
    SearchName: String(200),
    MinBalance: Decimal,
    MaxBalance: Decimal,
    IncludeInactive: Boolean DEFAULT FALSE
);

Idempotent script with OR MODIFY

CREATE OR MODIFY PERSISTENT ENTITY Sales.Customer (
    CustomerId: AutoNumber NOT NULL UNIQUE,
    Name: String(200) NOT NULL,
    Email: String(200),
    Phone: String(50)
);

If Sales.Customer already exists, the Phone attribute is added and existing attributes are updated. If it does not exist, it is created.

View entity with OQL query

CREATE VIEW ENTITY Reports.CustomerSummary (
    CustomerName: String,
    TotalOrders: Integer,
    TotalAmount: Decimal
) AS
    SELECT
        c.Name AS CustomerName,
        COUNT(o.OrderId) AS TotalOrders,
        SUM(o.Amount) AS TotalAmount
    FROM Sales.Customer c
    LEFT JOIN Sales.Order o ON o.Customer = c
    GROUP BY c.Name;

Entity with calculated attribute

CREATE PERSISTENT ENTITY Sales.Order (
    OrderNumber: String(50) NOT NULL UNIQUE,
    Subtotal: Decimal DEFAULT 0,
    TaxRate: Decimal DEFAULT 0,
    TotalAmount: Decimal CALCULATED BY Sales.CalcOrderTotal
);

Entity with composite index

CREATE PERSISTENT ENTITY Sales.Order (
    OrderId: AutoNumber NOT NULL UNIQUE,
    OrderNumber: String(50) NOT NULL UNIQUE,
    CustomerId: Long,
    OrderDate: DateTime,
    Status: Enumeration(Sales.OrderStatus)
)
INDEX (OrderNumber)
INDEX (CustomerId, OrderDate DESC)
INDEX (Status);

Entity with documentation

/**
 * Represents a customer order.
 * Orders track purchases from initial draft through delivery.
 */
@Position(300, 100)
CREATE PERSISTENT ENTITY Sales.Order (
    /** Auto-generated order identifier */
    OrderId: AutoNumber NOT NULL UNIQUE DEFAULT 1,

    /** Human-readable order number */
    OrderNumber: String(50) NOT NULL UNIQUE,

    OrderDate: DateTime NOT NULL,
    TotalAmount: Decimal DEFAULT 0,
    Status: Enumeration(Sales.OrderStatus) DEFAULT 'Draft',
    Notes: String(unlimited)
)
INDEX (OrderNumber)
INDEX (OrderDate DESC);

Notes

  • String requires an explicit length: String(200). Use String(unlimited) for unbounded text.
  • EXTENDS must appear before the opening parenthesis, not after the closing one.
  • Boolean attributes without an explicit DEFAULT automatically default to FALSE.
  • AutoNumber attributes are only valid on persistent entities.
  • The @Position annotation is optional and only affects the visual layout in Mendix Studio Pro.
  • Statements can be terminated with ; or / (Oracle-style).

See Also

ALTER ENTITY, DROP ENTITY, CREATE ASSOCIATION, CREATE ENUMERATION

ALTER ENTITY

Synopsis

ALTER ENTITY module.name ADD ( attribute_definition [, ...] )

ALTER ENTITY module.name DROP ( attribute_name [, ...] )

ALTER ENTITY module.name MODIFY ( attribute_definition [, ...] )

ALTER ENTITY module.name RENAME old_name TO new_name

ALTER ENTITY module.name ADD INDEX ( column [ ASC | DESC ] [, ...] )

ALTER ENTITY module.name DROP INDEX ( column [, ...] )

ALTER ENTITY module.name SET DOCUMENTATION 'text'

Description

ALTER ENTITY modifies an existing entity without replacing it entirely. This is useful for incremental changes to a domain model – adding a new attribute, removing an obsolete one, renaming for clarity, or managing indexes.

Each ALTER ENTITY statement performs exactly one operation. To make multiple changes, issue multiple statements.

The ADD operation appends one or more new attributes to the entity. Attribute definitions follow the same syntax as in CREATE ENTITY: a name, a data type, and optional constraints (NOT NULL, UNIQUE, DEFAULT).

The DROP operation removes one or more attributes by name. Dropping an attribute also removes any validation rules and index entries that reference it.

The MODIFY operation changes the type or constraints of existing attributes. The attribute name must already exist in the entity. The full attribute definition (type and constraints) replaces the current one.

The RENAME operation changes an attribute’s name. This updates references within the entity but does not automatically update microflows, pages, or access rules that reference the old name.

The ADD INDEX and DROP INDEX operations manage database indexes. Index columns may include an optional ASC or DESC sort direction.

The SET DOCUMENTATION operation replaces the entity’s documentation string.

Parameters

module.name
The qualified name of the entity to modify, in the form Module.EntityName.
attribute_definition
An attribute name followed by a colon, data type, and optional constraints. Same syntax as in CREATE ENTITY.
attribute_name
The name of an existing attribute to drop.
old_name
The current name of the attribute to rename.
new_name
The new name for the attribute.
column
An attribute name for the index, optionally followed by ASC or DESC.
text
A single-quoted string to use as the entity’s documentation.

Examples

Add new attributes

ALTER ENTITY Sales.Customer
    ADD (Phone: String(50), Notes: String(unlimited));

Drop an attribute

ALTER ENTITY Sales.Customer
    DROP (Notes);

Modify an attribute’s type

ALTER ENTITY Sales.Customer
    MODIFY (Phone: String(100) NOT NULL);

Rename an attribute

ALTER ENTITY Sales.Customer
    RENAME Phone TO PhoneNumber;

Add an index

ALTER ENTITY Sales.Customer
    ADD INDEX (Email);

Add a composite index with sort direction

ALTER ENTITY Sales.Order
    ADD INDEX (CustomerId, OrderDate DESC);

Drop an index

ALTER ENTITY Sales.Customer
    DROP INDEX (Email);

Set documentation

ALTER ENTITY Sales.Customer
    SET DOCUMENTATION 'Customer master data for the sales module.';

Notes

  • Each ALTER ENTITY statement performs a single operation. Chain multiple statements for multiple changes.
  • RENAME does not update references in microflows, pages, or access rules. Update those separately or use SHOW IMPACT OF to find affected elements.
  • DROP removes the attribute’s validation rules and index entries automatically.

See Also

CREATE ENTITY, DROP ENTITY

DROP ENTITY

Synopsis

DROP ENTITY module.name

Description

DROP ENTITY removes an entity from a module’s domain model. The entity and all of its attributes, validation rules, indexes, and access rules are deleted.

Associations that reference the dropped entity are not automatically removed. Drop those separately with DROP ASSOCIATION before or after dropping the entity to avoid dangling references.

Use SHOW IMPACT OF Module.EntityName to check which microflows, pages, and associations reference the entity before dropping it.

Parameters

module.name
The qualified name of the entity to remove, in the form Module.EntityName.

Examples

Drop a single entity

DROP ENTITY Sales.CustomerFilter;

Drop entity after checking impact

-- Check what references this entity
SHOW IMPACT OF Sales.OldEntity;

-- Drop related association first
DROP ASSOCIATION Sales.OldEntity_Customer;

-- Then drop the entity
DROP ENTITY Sales.OldEntity;

See Also

CREATE ENTITY, ALTER ENTITY, DROP ASSOCIATION

CREATE ENUMERATION

Synopsis

CREATE [ OR MODIFY ] ENUMERATION module.name (
    value_name 'caption'
    [, ...]
)

Description

CREATE ENUMERATION defines a new enumeration type in a module. An enumeration is a fixed set of named values that can be used as an attribute type on entities (via Enumeration(Module.EnumName)).

Each enumeration value has two parts: a value name (an identifier used in code and expressions) and a caption (a display string shown in the UI). The value name must be a valid identifier. The caption is a single-quoted string.

If OR MODIFY is specified, the statement is idempotent. If the enumeration already exists, its values are updated to match the definition.

A documentation comment (/** ... */) placed before the statement is preserved as the enumeration’s documentation.

Parameters

OR MODIFY
Makes the statement idempotent. If the enumeration already exists, its values are replaced with the new definition. Without this clause, creating a duplicate enumeration is an error.
module.name
The qualified name of the enumeration in the form Module.EnumerationName. The module must already exist.
value_name
An identifier for the enumeration value. Used in expressions and microflow logic.
caption
A single-quoted display string for the value. This is what end users see in the UI.

Examples

Basic enumeration

CREATE ENUMERATION Sales.OrderStatus (
    Draft 'Draft',
    Pending 'Pending Approval',
    Approved 'Approved',
    Shipped 'Shipped',
    Delivered 'Delivered',
    Cancelled 'Cancelled'
);

Enumeration with documentation

/** Priority levels for support tickets */
CREATE ENUMERATION Support.TicketPriority (
    Low 'Low',
    Medium 'Medium',
    High 'High',
    Critical 'Critical'
);

Idempotent with OR MODIFY

CREATE OR MODIFY ENUMERATION Sales.OrderStatus (
    Draft 'Draft',
    Pending 'Pending Approval',
    Approved 'Approved',
    Shipped 'Shipped',
    Delivered 'Delivered',
    Returned 'Returned',
    Cancelled 'Cancelled'
);

Using an enumeration as an attribute type

CREATE PERSISTENT ENTITY Sales.Order (
    OrderNumber: String(50) NOT NULL,
    Status: Enumeration(Sales.OrderStatus) DEFAULT 'Draft'
);

Notes

  • Enumeration values can also be managed incrementally with ALTER ENUMERATION ... ADD VALUE and ALTER ENUMERATION ... REMOVE VALUE.
  • The DEFAULT value on an entity attribute references the enumeration value name as a quoted string (e.g., DEFAULT 'Draft').

See Also

DROP ENUMERATION, CREATE ENTITY

DROP ENUMERATION

Synopsis

DROP ENUMERATION module.name

Description

DROP ENUMERATION removes an enumeration type from the project. Any entity attributes that reference the enumeration as their type (via Enumeration(Module.EnumName)) will become invalid after the enumeration is dropped.

Use SHOW IMPACT OF Module.EnumName to check which entities and attributes reference the enumeration before dropping it.

Parameters

module.name
The qualified name of the enumeration to remove, in the form Module.EnumerationName.

Examples

Drop an enumeration

DROP ENUMERATION Sales.OrderStatus;

Drop after checking references

SHOW IMPACT OF Sales.OldStatus;
DROP ENUMERATION Sales.OldStatus;

See Also

CREATE ENUMERATION

CREATE ASSOCIATION

Synopsis

CREATE [ OR MODIFY ] ASSOCIATION module.name
    FROM from_module.from_entity
    TO to_module.to_entity
    TYPE { Reference | ReferenceSet }
    [ OWNER { Default | Both | Parent | Child } ]
    [ DELETE_BEHAVIOR { DELETE_BUT_KEEP_REFERENCES | DELETE_CASCADE } ]

Description

CREATE ASSOCIATION defines a relationship between two entities in the domain model.

The FROM entity is the side that owns the foreign key (in database terms). The TO entity is the side being referenced. For a typical one-to-many relationship like “a Customer has many Orders,” the FROM side is the parent entity (Customer) and the TO side is the child entity (Order).

Two association types are supported:

  • Reference – a many-to-one relationship. Each object on the TO side references at most one object on the FROM side. Stored as a foreign key column in the TO entity’s database table.
  • ReferenceSet – a many-to-many relationship. Objects on both sides can reference multiple objects on the other side. Stored in a separate junction table.

The OWNER clause controls which side of the association can modify the relationship:

OwnerDescription
DefaultThe default owner (child/TO side) can set and clear the reference
BothBoth sides can modify the association
ParentOnly the FROM side can modify the association
ChildOnly the TO side can modify the association

The DELETE_BEHAVIOR clause controls what happens when an object on the FROM side is deleted:

BehaviorDescription
DELETE_BUT_KEEP_REFERENCESDelete the object and set references to null
DELETE_CASCADEDelete the object and all associated objects on the TO side

If OR MODIFY is specified, the statement is idempotent: if the association already exists, it is updated to match the new definition.

A documentation comment (/** ... */) placed before the statement is preserved as the association’s documentation.

Parameters

OR MODIFY
Makes the statement idempotent. If the association already exists, its properties are updated. Without this clause, creating a duplicate association is an error.
module.name
The qualified name of the association in the form Module.AssociationName. By convention, association names follow the pattern Module.Child_Parent or Module.EntityA_EntityB.
FROM from_module.from_entity
The entity on the “from” side of the relationship. This is the entity that owns the foreign key in the database. In a one-to-many relationship, this is typically the “one” (parent) side.
TO to_module.to_entity
The entity on the “to” side of the relationship. In a one-to-many relationship, this is typically the “many” (child) side that holds the reference.
TYPE
Either Reference (many-to-one) or ReferenceSet (many-to-many).
OWNER
Which side can modify the association. Defaults to Default if omitted.
DELETE_BEHAVIOR
What happens to associated objects when a FROM-side object is deleted. If omitted, references are kept (the default Mendix behavior).

Examples

Many-to-one: Order belongs to Customer

CREATE ASSOCIATION Sales.Order_Customer
    FROM Sales.Customer
    TO Sales.Order
    TYPE Reference
    OWNER Default
    DELETE_BEHAVIOR DELETE_BUT_KEEP_REFERENCES;

Many-to-many: Order has Products

CREATE ASSOCIATION Sales.Order_Product
    FROM Sales.Order
    TO Sales.Product
    TYPE ReferenceSet
    OWNER Both;

Cascade delete: Invoice deleted with Order

/** Invoice must be deleted when its Order is deleted */
CREATE ASSOCIATION Sales.Order_Invoice
    FROM Sales.Order
    TO Sales.Invoice
    TYPE Reference
    DELETE_BEHAVIOR DELETE_CASCADE;

Idempotent with OR MODIFY

CREATE OR MODIFY ASSOCIATION Sales.Order_Customer
    FROM Sales.Customer
    TO Sales.Order
    TYPE Reference
    OWNER Default;

Cross-module association

CREATE ASSOCIATION HR.Employee_Department
    FROM HR.Department
    TO HR.Employee
    TYPE Reference;

Notes

  • The naming convention Module.Child_Parent reflects that the child (TO) entity holds the reference to the parent (FROM) entity. This can be counterintuitive – the FROM entity is the parent, and the TO entity is the child.
  • Internally, Mendix stores the FROM entity pointer as ParentPointer and the TO entity pointer as ChildPointer. These BSON field names are inverted relative to what you might expect.
  • Entity access rules for associations (member access) must only be added to the FROM entity. Adding them to the TO entity triggers validation errors.

See Also

DROP ASSOCIATION, CREATE ENTITY

DROP ASSOCIATION

Synopsis

DROP ASSOCIATION module.name

Description

DROP ASSOCIATION removes an association (relationship) between two entities from the domain model. The entities themselves are not affected – only the relationship is removed.

After dropping an association, any microflows, pages, or access rules that traverse it will become invalid. Use SHOW IMPACT OF Module.AssociationName to check references before dropping.

Parameters

module.name
The qualified name of the association to remove, in the form Module.AssociationName.

Examples

Drop an association

DROP ASSOCIATION Sales.Order_Customer;

Drop after checking impact

SHOW IMPACT OF Sales.OldAssociation;
DROP ASSOCIATION Sales.OldAssociation;

See Also

CREATE ASSOCIATION, DROP ENTITY

CREATE CONSTANT

Synopsis

CREATE [ OR MODIFY ] CONSTANT module.name TYPE data_type [ DEFAULT value ]

Description

CREATE CONSTANT defines a named constant in a module. Constants hold configuration values (API URLs, feature flags, limits) that can differ between environments. In Mendix, constant values can be overridden per runtime configuration without changing the model.

The TYPE clause specifies the constant’s data type. Supported types include String, Integer, Long, Decimal, Boolean, and DateTime.

The optional DEFAULT clause sets the constant’s default value. This is the value used at runtime unless overridden by environment configuration.

If OR MODIFY is specified, the statement is idempotent. If the constant already exists, its type and default value are updated.

Constant values can also be overridden per deployment configuration using ALTER SETTINGS CONSTANT.

Parameters

OR MODIFY
Makes the statement idempotent. If the constant already exists, its definition is updated. Without this clause, creating a duplicate constant is an error.
module.name
The qualified name of the constant in the form Module.ConstantName. The module must already exist.
data_type
The constant’s data type. One of: String, Integer, Long, Decimal, Boolean, DateTime.
DEFAULT value
The default value for the constant. String values are single-quoted. Numeric values are bare. Boolean values are true or false.

Examples

String constant for API URL

CREATE CONSTANT MyModule.ApiBaseUrl TYPE String DEFAULT 'https://api.example.com';

Integer constant for configuration

CREATE CONSTANT MyModule.MaxRetries TYPE Integer DEFAULT 3;

Boolean feature flag

CREATE CONSTANT MyModule.EnableLogging TYPE Boolean DEFAULT true;

Idempotent with OR MODIFY

CREATE OR MODIFY CONSTANT MyModule.ApiBaseUrl TYPE String DEFAULT 'https://api.example.com/v2';

Constant without a default

CREATE CONSTANT MyModule.DatabasePassword TYPE String;

Override constant per configuration

-- Create the constant
CREATE CONSTANT MyModule.ApiBaseUrl TYPE String DEFAULT 'https://api.example.com';

-- Override in a specific runtime configuration
ALTER SETTINGS CONSTANT 'MyModule.ApiBaseUrl' VALUE 'https://staging.example.com' IN CONFIGURATION 'Staging';

See Also

CREATE ENTITY, CREATE ENUMERATION

Microflow Statements

Statements for creating and dropping microflows, nanoflows, and Java actions.

Microflows are the primary mechanism for implementing business logic in Mendix applications. They execute on the server and have access to the full range of activities including database operations, integrations, and security-sensitive actions.

Nanoflows share the same syntax as microflows but execute on the client (browser or native mobile). They have a restricted set of available activities.

StatementDescription
CREATE MICROFLOWCreate a microflow with activities and control flow
CREATE NANOFLOWCreate a client-side nanoflow
CREATE JAVA ACTIONCreate a Java action stub with inline code
DROP MICROFLOW / NANOFLOWRemove a microflow or nanoflow

CREATE MICROFLOW

Synopsis

CREATE [ OR REPLACE ] MICROFLOW module.Name
    [ ( DECLARE $param : type [, ...] ) ]
    [ RETURN type ]
    [ FOLDER 'path' ]
BEGIN
    statements
END

Description

Creates a new microflow in the specified module. Microflows are the server-side logic building blocks in Mendix – they execute on the application server and have access to the full set of activities including database operations, external service calls, and security-sensitive actions.

If OR REPLACE is specified and a microflow with the same qualified name already exists, it is replaced. Otherwise, creating a microflow with an existing name is an error.

The microflow body consists of a sequence of statements enclosed in BEGIN ... END. Statements are executed in order, with control flow managed by IF, LOOP, WHILE, and RETURN constructs.

Microflow Parameters

Microflow parameters are declared in parentheses after the name. Each parameter has a name (prefixed with $), a colon, and a type. Parameters become variables available throughout the microflow body.

Return Type

If the microflow returns a value, specify RETURN type after the parameter list. The type can be any primitive type, an entity type, or a list type. Every execution path must end with a RETURN statement when a return type is declared.

Folder Placement

The optional FOLDER clause places the microflow in a subfolder within the module. Nested folders use / as separator. Missing folders are created automatically.

Activities

The following activities are available inside the microflow body.

Variable Declaration and Assignment

DECLARE $Var Type = value;
DECLARE $Entity Module.Entity;
DECLARE $List List of Module.Entity = empty;
SET $Var = expression;

Primitive types: String, Integer, Long, Decimal, Boolean, DateTime. Entity declarations do not use = empty. List declarations require = empty to initialize an empty list.

Object Operations

$Var = CREATE Module.Entity ( Attr1 = value1, Attr2 = value2 );
CHANGE $Entity ( Attr = value );
COMMIT $Entity [ WITH EVENTS ] [ REFRESH ];
DELETE $Entity;
ROLLBACK $Entity [ REFRESH ];

CREATE instantiates a new object with initial attribute values. CHANGE modifies attributes on an existing object. COMMIT persists changes to the database – WITH EVENTS triggers before/after commit event handlers, REFRESH updates client-side state. ROLLBACK reverts uncommitted changes to an object.

Retrieval

-- Database retrieve with optional XPath constraint
RETRIEVE $Var FROM Module.Entity [ WHERE condition ] [ LIMIT n ];

-- Retrieve by association
RETRIEVE $List FROM $Parent/Module.AssocName;

RETRIEVE ... LIMIT 1 returns a single entity. Without LIMIT or with LIMIT greater than 1, it returns a list. Retrieve by association traverses an association from a known object.

Calls

$Result = CALL MICROFLOW Module.Name ( Param = $value );
$Result = CALL NANOFLOW Module.Name ( Param = $value );
$Result = CALL JAVA ACTION Module.Name ( Param = value );

Call another microflow, nanoflow, or Java action. Parameters are passed by name. The result can be assigned to a variable when the callee has a return type. If no return value is needed, omit the $Result = prefix.

UI Actions

SHOW PAGE Module.PageName ( $Param = $value );
CLOSE PAGE;

SHOW PAGE opens a page, passing parameters by name. CLOSE PAGE closes the current page.

Validation and Logging

VALIDATION FEEDBACK $Entity/Attribute MESSAGE 'message';
LOG INFO | WARNING | ERROR [ NODE 'name' ] 'message';

VALIDATION FEEDBACK adds a validation error to a specific attribute on an object. LOG writes to the application log at the specified level, optionally tagged with a log node name.

Execute Database Query

$Result = EXECUTE DATABASE QUERY Module.Connector.QueryName;

Executes a Database Connector query. The three-part name identifies the connector module, connection, and query. Supports DYNAMIC, parameters, and CONNECTION override.

Control Flow

IF condition THEN
    statements
[ ELSE
    statements ]
END IF;

LOOP $Item IN $List BEGIN
    statements
END LOOP;

WHILE condition BEGIN
    statements
END WHILE;

RETURN $value;

IF branches on a boolean expression. LOOP iterates over each item in a list. WHILE loops while a condition holds true. RETURN ends execution and returns a value (required when the microflow declares a return type).

Error Handling

-- Suffix on any activity (except EXECUTE DATABASE QUERY)
activity ON ERROR CONTINUE;
activity ON ERROR ROLLBACK;
activity ON ERROR {
    handler_statements
};

Error handling is attached as a suffix to an individual activity. ON ERROR CONTINUE suppresses the error and continues. ON ERROR ROLLBACK rolls back the current transaction. ON ERROR { ... } executes custom error-handling logic.

Annotations

Annotations are placed before an activity to control visual appearance in the microflow editor:

@position(x, y)          -- Canvas position
@caption 'text'          -- Custom caption
@color Green             -- Background color
@annotation 'text'       -- Visual note attached to next activity

Parameters

module.Name
The qualified name of the microflow (Module.MicroflowName). The module must already exist.
$param : type
A parameter declaration. The name must start with $. The type can be:
  • Primitive: String, Integer, Long, Decimal, Boolean, DateTime
  • Entity: Module.EntityName
  • List: List of Module.EntityName
  • Enumeration: Enumeration(Module.EnumName)
RETURN type
The return type of the microflow. Same type options as parameters.
FOLDER 'path'
Optional folder path within the module. Nested folders use / separator (e.g., 'Orders/Processing').

Examples

Simple microflow that creates and commits an object:

CREATE MICROFLOW Sales.ACT_CreateOrder
FOLDER 'Orders'
BEGIN
    DECLARE $Order Sales.Order;
    $Order = CREATE Sales.Order (
        OrderDate = [%CurrentDateTime%],
        Status = 'Draft'
    );
    COMMIT $Order;
    SHOW PAGE Sales.Order_Edit ($Order = $Order);
    RETURN $Order;
END;

Microflow with parameters and conditional logic:

CREATE MICROFLOW Sales.ACT_ApproveOrder
    (DECLARE $Order: Sales.Order)
    RETURN Boolean
BEGIN
    IF $Order/Status = 'Pending' THEN
        CHANGE $Order (Status = 'Approved');
        COMMIT $Order WITH EVENTS;
        LOG INFO NODE 'OrderProcessing' 'Order approved';
        RETURN true;
    ELSE
        VALIDATION FEEDBACK $Order/Status MESSAGE 'Only pending orders can be approved';
        RETURN false;
    END IF;
END;

Microflow with a loop over a retrieved list:

CREATE MICROFLOW Sales.ACT_DeactivateExpiredCustomers
    RETURN Integer
FOLDER 'Scheduled'
BEGIN
    DECLARE $Count Integer = 0;
    RETRIEVE $Customers FROM Sales.Customer
        WHERE [IsActive = true AND ExpiryDate < [%CurrentDateTime%]];
    LOOP $Customer IN $Customers BEGIN
        CHANGE $Customer (IsActive = false);
        COMMIT $Customer;
        SET $Count = $Count + 1;
    END LOOP;
    LOG INFO NODE 'Maintenance' 'Deactivated expired customers';
    RETURN $Count;
END;

Microflow with error handling:

CREATE MICROFLOW Integration.ACT_SyncData
BEGIN
    DECLARE $Result Integration.SyncResult;
    $Result = CREATE Integration.SyncResult (
        StartTime = [%CurrentDateTime%],
        Status = 'Running'
    );
    COMMIT $Result;

    $Result = CALL MICROFLOW Integration.SUB_FetchExternalData (
        SyncResult = $Result
    ) ON ERROR {
        CHANGE $Result (Status = 'Failed');
        COMMIT $Result;
        LOG ERROR NODE 'Integration' 'Sync failed';
    };

    CHANGE $Result (
        Status = 'Completed',
        EndTime = [%CurrentDateTime%]
    );
    COMMIT $Result;
END;

Microflow calling a Java action:

CREATE MICROFLOW MyModule.ACT_GenerateReport
    (DECLARE $StartDate: DateTime, DECLARE $EndDate: DateTime)
    RETURN String
BEGIN
    DECLARE $Report String = '';
    $Report = CALL JAVA ACTION MyModule.JA_GenerateReport (
        StartDate = $StartDate,
        EndDate = $EndDate
    );
    RETURN $Report;
END;

Using OR REPLACE to update an existing microflow:

CREATE OR REPLACE MICROFLOW Sales.ACT_CreateOrder
FOLDER 'Orders'
BEGIN
    DECLARE $Order Sales.Order;
    $Order = CREATE Sales.Order (
        OrderDate = [%CurrentDateTime%],
        Status = 'Draft',
        CreatedBy = '[%CurrentUser%]'
    );
    COMMIT $Order WITH EVENTS;
    RETURN $Order;
END;

See Also

CREATE NANOFLOW, DROP MICROFLOW, CREATE JAVA ACTION, DESCRIBE MICROFLOW, GRANT EXECUTE ON MICROFLOW

CREATE NANOFLOW

Synopsis

CREATE [ OR REPLACE ] NANOFLOW module.Name
    [ ( DECLARE $param : type [, ...] ) ]
    [ RETURN type ]
    [ FOLDER 'path' ]
BEGIN
    statements
END

Description

Creates a new nanoflow in the specified module. Nanoflows share the same syntax as microflows but execute on the client (web browser or native mobile app) rather than on the server. This makes them suitable for offline-capable logic, instant UI feedback, and reducing server round-trips.

If OR REPLACE is specified and a nanoflow with the same qualified name already exists, it is replaced.

Restricted Activity Set

Because nanoflows run on the client, several server-side activities are not available:

  • No direct database retrieval with XPath (RETRIEVE ... FROM Entity WHERE ... over database)
  • No COMMIT with database transactions
  • No LOG statements (server log is inaccessible)
  • No EXECUTE DATABASE QUERY
  • No server-side Java action calls

Available activities in nanoflows include:

  • CREATE, CHANGE, DELETE (on client-side objects)
  • COMMIT (triggers synchronization when online)
  • RETRIEVE by association
  • CALL NANOFLOW (call other nanoflows)
  • CALL MICROFLOW (triggers a server round-trip)
  • SHOW PAGE, CLOSE PAGE
  • VALIDATION FEEDBACK
  • IF, LOOP, WHILE, RETURN
  • Variable declaration and assignment

Parameters, Return Type, and Folder

These work identically to CREATE MICROFLOW. See that page for full details on types, annotations, and folder placement.

Parameters

module.Name
The qualified name of the nanoflow (Module.NanoflowName). The module must already exist.
$param : type
A parameter declaration. Same types as microflow parameters: primitives, entities, lists, enumerations.
RETURN type
The return type of the nanoflow.
FOLDER 'path'
Optional folder path within the module.

Examples

Nanoflow that validates an object on the client:

CREATE NANOFLOW Sales.NAV_ValidateOrder
    (DECLARE $Order: Sales.Order)
    RETURN Boolean
BEGIN
    IF $Order/CustomerName = empty THEN
        VALIDATION FEEDBACK $Order/CustomerName MESSAGE 'Customer name is required';
        RETURN false;
    END IF;
    IF $Order/Amount <= 0 THEN
        VALIDATION FEEDBACK $Order/Amount MESSAGE 'Amount must be positive';
        RETURN false;
    END IF;
    RETURN true;
END;

Nanoflow that toggles a UI state:

CREATE NANOFLOW MyModule.NAV_ToggleDetail
    (DECLARE $Helper: MyModule.UIHelper)
BEGIN
    IF $Helper/ShowDetail = true THEN
        CHANGE $Helper (ShowDetail = false);
    ELSE
        CHANGE $Helper (ShowDetail = true);
    END IF;
END;

Nanoflow calling a microflow for server-side work:

CREATE NANOFLOW Sales.NAV_SubmitOrder
    (DECLARE $Order: Sales.Order)
BEGIN
    CHANGE $Order (Status = 'Submitted');
    $Result = CALL MICROFLOW Sales.ACT_ProcessOrder (Order = $Order);
    SHOW PAGE Sales.Order_Confirmation ($Order = $Order);
END;

See Also

CREATE MICROFLOW, DROP MICROFLOW, GRANT EXECUTE ON NANOFLOW

DROP MICROFLOW / NANOFLOW

Synopsis

DROP MICROFLOW module.Name

DROP NANOFLOW module.Name

Description

Removes a microflow or nanoflow from the project. The microflow or nanoflow must exist; otherwise an error is raised.

Dropping a microflow or nanoflow does not automatically update references to it from other microflows, pages, navigation, or security rules. Use SHOW IMPACT OF module.Name before dropping to identify all references that will break.

Parameters

module.Name
The qualified name of the microflow or nanoflow to drop (Module.MicroflowName or Module.NanoflowName).

Examples

Drop a microflow:

DROP MICROFLOW Sales.ACT_CreateOrder;

Drop a nanoflow:

DROP NANOFLOW Sales.NAV_ValidateOrder;

Check impact before dropping:

-- See what references this microflow
SHOW IMPACT OF Sales.ACT_CreateOrder;

-- Then drop if safe
DROP MICROFLOW Sales.ACT_CreateOrder;

See Also

CREATE MICROFLOW, CREATE NANOFLOW, SHOW IMPACT

CREATE JAVA ACTION

Synopsis

CREATE JAVA ACTION module.Name ( parameters )
    RETURNS type
    [ EXPOSED AS 'caption' IN 'category' ]
    AS $$ java_code $$

DROP JAVA ACTION module.Name

Description

Creates a Java action with inline Java code. Java actions allow custom server-side logic written in Java to be called from microflows.

The action parameters, return type, and Java body are all specified inline. When the action is created, the corresponding Java source file is generated with the parameter boilerplate and the provided code body.

Type Parameters

Java actions support generic entity handling through type parameters. A parameter declared as ENTITY <pEntity> creates a type parameter selector – the caller chooses which entity type to use. Other parameters declared as bare pEntity (without ENTITY) receive instances of the selected entity type.

Exposed Actions

The optional EXPOSED AS clause makes the action visible in the Studio Pro toolbox under the specified category, allowing modelers to use it directly in microflows.

Parameters

module.Name
The qualified name of the Java action.
parameters
Comma-separated parameter declarations. Each parameter has a name, colon, and type. Supported types:
  • Primitives: String, Integer, Long, Decimal, Boolean, DateTime
  • Entity: Module.EntityName
  • List: List of Module.EntityName
  • Enumeration: ENUM Module.EnumName or Enumeration(Module.EnumName)
  • String template: StringTemplate(Sql), StringTemplate(Oql)
  • Type parameter declaration: ENTITY <pEntity> (declares the type parameter)
  • Type parameter reference: bare pEntity (uses the declared type parameter)
  • Add NOT NULL after the type to mark a parameter as required.
RETURNS type
The return type. Same type options as parameters.
EXPOSED AS 'caption' IN 'category'
Optional. Makes the action visible in the toolbox with the given caption and category.
AS $$ java_code $$
The Java code body, enclosed in $$ delimiters.

Examples

Simple Java action returning a string:

CREATE JAVA ACTION MyModule.JA_FormatCurrency (
    Amount: Decimal NOT NULL,
    CurrencyCode: String NOT NULL
) RETURNS String
AS $$
    java.text.NumberFormat formatter = java.text.NumberFormat.getCurrencyInstance();
    formatter.setCurrency(java.util.Currency.getInstance(CurrencyCode));
    return formatter.format(Amount);
$$;

Java action with type parameters for generic entity handling:

CREATE JAVA ACTION MyModule.JA_Validate (
    EntityType: ENTITY <pEntity> NOT NULL,
    InputObject: pEntity NOT NULL
) RETURNS Boolean
EXPOSED AS 'Validate Entity' IN 'Validation'
AS $$
    return InputObject != null;
$$;

Java action with a list parameter:

CREATE JAVA ACTION MyModule.JA_ExportToCsv (
    Records: List of MyModule.Customer NOT NULL,
    FilePath: String NOT NULL
) RETURNS Boolean
AS $$
    // Export logic here
    return true;
$$;

Drop a Java action:

DROP JAVA ACTION MyModule.JA_FormatCurrency;

See Also

CREATE MICROFLOW, SHOW JAVA ACTIONS

Page Statements

Statements for creating, modifying, and dropping pages, snippets, and layouts.

Pages define the user interface of a Mendix application. Each page has a layout, a title, optional parameters, and a widget tree that describes the UI structure. Snippets are reusable widget fragments that can be embedded in pages.

StatementDescription
CREATE PAGECreate a page with a widget tree
CREATE SNIPPETCreate a reusable widget fragment
ALTER PAGE / ALTER SNIPPETModify an existing page or snippet in-place
DROP PAGE / SNIPPETRemove a page or snippet
CREATE LAYOUTCreate a page layout

CREATE PAGE

Synopsis

CREATE [ OR REPLACE ] PAGE module.Name
(
    [ Params: { $param : Module.Entity [, ...] }, ]
    Title: 'title',
    Layout: Module.LayoutName
    [, Folder: 'path' ]
    [, Variables: { $name : type = 'expression' [, ...] } ]
)
{
    widget_tree
}

Description

Creates a new page in the specified module. A page defines a user interface screen with a title, a layout, optional parameters, and a hierarchical widget tree.

If OR REPLACE is specified and a page with the same qualified name already exists, it is replaced. Otherwise, creating a page with an existing name is an error.

Page Properties

The page declaration block ( ... ) contains comma-separated key-value properties:

  • Params – Page parameters. Each parameter has a $-prefixed name and an entity type. Parameters are passed when the page is opened (e.g., from a microflow SHOW PAGE or from a data view).
  • Title – The page title displayed in the browser tab or page header.
  • Layout – The layout the page is based on. Must be a qualified name referring to an existing layout (e.g., Atlas_Core.Atlas_Default, Atlas_Core.PopupLayout).
  • Folder – Optional folder path within the module. Nested folders use / separator.
  • Variables – Optional page-level variables with default expressions. Variables can be used for conditional visibility and dynamic behavior.

Widget Tree

The widget tree inside { ... } defines the page content. Widgets are nested hierarchically. Each widget has:

  • A type keyword (e.g., DATAVIEW, TEXTBOX, ACTIONBUTTON)
  • A name (unique within the page, used for ALTER PAGE references)
  • Properties in parentheses (Key: value, ...)
  • Optional children in braces { ... }

Widget Types

Layout Widgets

WidgetDescriptionKey Properties
LAYOUTGRIDResponsive grid containerClass, Style
ROWGrid rowClass
COLUMNGrid columnClass
CONTAINERGeneric div containerClass, Style, DesignProperties
CUSTOMCONTAINERCustom container widgetClass, Style

Data Widgets

WidgetDescriptionKey Properties
DATAVIEWDisplays/edits a single objectDataSource
LISTVIEWRenders a list of objectsDataSource, PageSize
DATAGRIDTabular data display with columnsDataSource, PageSize, Pagination
GALLERYCard-based list displayDataSource, PageSize

Input Widgets

WidgetDescriptionKey Properties
TEXTBOXSingle-line text inputLabel, Attribute
TEXTAREAMulti-line text inputLabel, Attribute
CHECKBOXBoolean toggleLabel, Attribute
RADIOBUTTONSRadio button groupLabel, Attribute
DATEPICKERDate/time selectorLabel, Attribute
COMBOBOXDropdown selectorLabel, Attribute

Display Widgets

WidgetDescriptionKey Properties
DYNAMICTEXTDisplay-only text bound to an attributeAttribute
IMAGEGeneric imageWidth, Height
STATICIMAGEFixed image from project resourcesWidth, Height
DYNAMICIMAGEImage from an entity attributeWidth, Height

Action Widgets

WidgetDescriptionKey Properties
ACTIONBUTTONButton that triggers an actionCaption, Action, ButtonStyle
LINKBUTTONHyperlink-styled actionCaption, Action

Structure Widgets

WidgetDescriptionKey Properties
HEADERPage header area
FOOTERPage footer area (typically for buttons)
CONTROLBARControl bar for data grids
SNIPPETCALLEmbeds a snippetSnippet: Module.SnippetName
NAVIGATIONLISTNavigation sidebar list

DataSource Types

The DataSource property determines how a data widget obtains its data:

DataSourceSyntaxDescription
ParameterDataSource: $ParamNameBinds to a page parameter or enclosing data view
DatabaseDataSource: DATABASE Module.EntityRetrieves from database
SelectionDataSource: SELECTION widgetNameListens to selection on another widget
MicroflowDataSource: MICROFLOW Module.MFNameCalls a microflow for data

Action Types

The Action property on buttons determines what happens when clicked:

ActionSyntaxDescription
SaveAction: SAVE_CHANGESCommits and closes
CancelAction: CANCEL_CHANGESRolls back and closes
MicroflowAction: MICROFLOW Module.Name(Param: val)Calls a microflow
NanoflowAction: NANOFLOW Module.Name(Param: val)Calls a nanoflow
PageAction: PAGE Module.PageNameOpens a page
CloseAction: CLOSE_PAGECloses the current page
DeleteAction: DELETEDeletes the context object

ButtonStyle Values

Primary, Default, Success, Danger, Warning, Info.

DataGrid Columns

Inside a DATAGRID, child widgets define columns. Each column widget specifies:

PropertyValuesDescription
Attributeattribute nameThe entity attribute to display
CaptionstringColumn header (defaults to attribute name)
Alignmentleft, center, rightText alignment
WrapTexttrue, falseWhether text wraps
Sortabletrue, falseWhether column is sortable
Resizabletrue, falseWhether column is resizable
Draggabletrue, falseWhether column is draggable
Hidableyes, hidden, noColumn visibility control
ColumnWidthautoFill, autoFit, manualWidth mode
Sizeinteger (px)Manual width in pixels
Visibleexpression stringConditional visibility
Tooltiptext stringColumn tooltip

Common Widget Properties

These properties are available on most widget types:

PropertyDescriptionExample
ClassCSS class namesClass: 'card mx-spacing-top-large'
StyleInline CSSStyle: 'padding: 16px;'
EditableEdit controlEditable: NEVER or Editable: ALWAYS
VisibleVisibility expressionVisible: '$showField'
DesignPropertiesAtlas design propertiesDesignProperties: ['Spacing top': 'Large']

Parameters

module.Name
The qualified name of the page (Module.PageName). The module must already exist.
Params: { ... }
Optional page parameters. Each parameter has a $-prefixed name and an entity type.
Title: 'title'
The display title of the page. Required.
Layout: Module.LayoutName
The page layout. Must reference an existing layout. Required.
Folder: 'path'
Optional folder within the module. Nested folders use /.
Variables: { ... }
Optional page variables with type and default expression.

Examples

Edit page with a data view and form fields:

CREATE PAGE MyModule.Customer_Edit
(
    Params: { $Customer: MyModule.Customer },
    Title: 'Edit Customer',
    Layout: Atlas_Core.PopupLayout
)
{
    DATAVIEW dvCustomer (DataSource: $Customer) {
        TEXTBOX txtName (Label: 'Name', Attribute: Name)
        TEXTBOX txtEmail (Label: 'Email', Attribute: Email)
        COMBOBOX cbStatus (Label: 'Status', Attribute: Status)

        FOOTER footer1 {
            ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)
            ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES)
        }
    }
};

Overview page with a data grid:

CREATE PAGE Sales.Order_Overview
(
    Title: 'Orders',
    Layout: Atlas_Core.Atlas_Default,
    Folder: 'Orders'
)
{
    DATAGRID dgOrders (DataSource: DATABASE Sales.Order, PageSize: 20) {
        COLUMN colId (Attribute: OrderId, Caption: 'Order #')
        COLUMN colDate (Attribute: OrderDate, Caption: 'Date')
        COLUMN colStatus (Attribute: Status, Caption: 'Status')
        COLUMN colAmount (Attribute: TotalAmount, Caption: 'Amount', Alignment: right)
        CONTROLBAR cb1 {
            ACTIONBUTTON btnNew (Caption: 'New Order', Action: PAGE Sales.Order_Edit, ButtonStyle: Primary)
        }
    }
};

Page with layout grid and containers:

CREATE PAGE MyModule.Dashboard
(
    Title: 'Dashboard',
    Layout: Atlas_Core.Atlas_Default
)
{
    LAYOUTGRID lgMain {
        ROW row1 {
            COLUMN col1 (Class: 'col-md-8') {
                CONTAINER cntRecent (Class: 'card') {
                    LISTVIEW lvRecent (DataSource: DATABASE MyModule.RecentActivity, PageSize: 5) {
                        DYNAMICTEXT txtDesc (Attribute: Description)
                    }
                }
            }
            COLUMN col2 (Class: 'col-md-4') {
                CONTAINER cntStats (Class: 'card') {
                    DYNAMICTEXT txtCount (Attribute: TotalCount)
                }
            }
        }
    }
};

Page with snippet call:

CREATE PAGE MyModule.Customer_Detail
(
    Params: { $Customer: MyModule.Customer },
    Title: 'Customer Detail',
    Layout: Atlas_Core.Atlas_Default
)
{
    DATAVIEW dvCustomer (DataSource: $Customer) {
        SNIPPETCALL snpHeader (Snippet: MyModule.CustomerHeader)
        TEXTBOX txtName (Label: 'Name', Attribute: Name)
        TEXTBOX txtEmail (Label: 'Email', Attribute: Email)
    }
};

Page with a gallery and selection-driven detail:

CREATE PAGE Sales.Product_Gallery
(
    Title: 'Products',
    Layout: Atlas_Core.Atlas_Default
)
{
    LAYOUTGRID lgMain {
        ROW row1 {
            COLUMN col1 (Class: 'col-md-6') {
                GALLERY galProducts (DataSource: DATABASE Sales.Product, PageSize: 12) {
                    DYNAMICTEXT txtName (Attribute: Name)
                    DYNAMICTEXT txtPrice (Attribute: Price)
                }
            }
            COLUMN col2 (Class: 'col-md-6') {
                DATAVIEW dvDetail (DataSource: SELECTION galProducts) {
                    TEXTBOX txtName (Label: 'Product', Attribute: Name)
                    TEXTBOX txtDesc (Label: 'Description', Attribute: Description)
                }
            }
        }
    }
};

Page with page variables and conditional visibility:

CREATE PAGE MyModule.AdvancedForm
(
    Params: { $Item: MyModule.Item },
    Title: 'Advanced Form',
    Layout: Atlas_Core.Atlas_Default,
    Variables: { $showAdvanced: Boolean = 'false' }
)
{
    DATAVIEW dvItem (DataSource: $Item) {
        TEXTBOX txtName (Label: 'Name', Attribute: Name)
        ACTIONBUTTON btnToggle (Caption: 'Show Advanced', Action: NANOFLOW MyModule.NAV_Toggle)
        CONTAINER cntAdvanced (Visible: '$showAdvanced') {
            TEXTAREA taNotes (Label: 'Notes', Attribute: Notes)
        }
        FOOTER footer1 {
            ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)
            ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES)
        }
    }
};

Page with microflow data source:

CREATE PAGE Reports.MonthlySummary
(
    Title: 'Monthly Summary',
    Layout: Atlas_Core.Atlas_Default
)
{
    DATAVIEW dvReport (DataSource: MICROFLOW Reports.DS_GetMonthlySummary) {
        DYNAMICTEXT txtPeriod (Attribute: Period)
        DYNAMICTEXT txtRevenue (Attribute: TotalRevenue)
        DYNAMICTEXT txtOrders (Attribute: OrderCount)
    }
};

See Also

CREATE SNIPPET, ALTER PAGE, DROP PAGE, DESCRIBE PAGE, GRANT VIEW ON PAGE

CREATE SNIPPET

Synopsis

CREATE [ OR REPLACE ] SNIPPET module.Name
(
    [ Params: { $param : Module.Entity [, ...] } ]
    [, Folder: 'path' ]
)
{
    widget_tree
}

Description

Creates a reusable widget fragment (snippet) in the specified module. Snippets contain a widget tree that can be embedded in pages or other snippets using the SNIPPETCALL widget.

If OR REPLACE is specified and a snippet with the same qualified name already exists, it is replaced.

Snippets support the same widget types and syntax as pages. The key difference is that snippets do not have a Title or Layout – they are fragments meant to be included within a page’s layout.

Snippet Parameters

Snippets can optionally declare parameters. When a snippet has parameters, the SNIPPETCALL widget that embeds it must provide the corresponding values.

Folder Placement

The optional Folder property places the snippet in a subfolder within the module.

Parameters

module.Name
The qualified name of the snippet (Module.SnippetName). The module must already exist.
Params: { ... }
Optional snippet parameters. Each parameter has a $-prefixed name and an entity type.
Folder: 'path'
Optional folder path within the module.

Examples

Simple snippet with a header:

CREATE SNIPPET MyModule.CustomerHeader
(
    Params: { $Customer: MyModule.Customer }
)
{
    CONTAINER cntHeader (Class: 'card-header') {
        DYNAMICTEXT txtName (Attribute: Name)
        DYNAMICTEXT txtEmail (Attribute: Email)
    }
};

Snippet with form fields:

CREATE SNIPPET MyModule.AddressFields
(
    Params: { $Address: MyModule.Address }
)
{
    TEXTBOX txtStreet (Label: 'Street', Attribute: Street)
    TEXTBOX txtCity (Label: 'City', Attribute: City)
    TEXTBOX txtZip (Label: 'Zip Code', Attribute: ZipCode)
    TEXTBOX txtCountry (Label: 'Country', Attribute: Country)
};

Snippet without parameters:

CREATE SNIPPET MyModule.AppFooter
{
    CONTAINER cntFooter (Class: 'app-footer') {
        DYNAMICTEXT txtVersion (Attribute: Version)
    }
};

Embedding a snippet in a page:

CREATE PAGE MyModule.Customer_Edit
(
    Params: { $Customer: MyModule.Customer },
    Title: 'Edit Customer',
    Layout: Atlas_Core.PopupLayout
)
{
    DATAVIEW dvCustomer (DataSource: $Customer) {
        SNIPPETCALL snpAddress (Snippet: MyModule.AddressFields)
        FOOTER footer1 {
            ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)
        }
    }
};

See Also

CREATE PAGE, ALTER PAGE, DROP PAGE

ALTER PAGE / ALTER SNIPPET

Synopsis

ALTER PAGE module.Name {
    operations
}

ALTER SNIPPET module.Name {
    operations
}

Where each operation is one of:

-- Set a property on a widget
SET property = value ON widgetName;
SET ( property1 = value1, property2 = value2 ) ON widgetName;

-- Set a page-level property (no ON clause)
SET Title = 'New Title';

-- Set a pluggable widget property (quoted name)
SET 'propertyName' = value ON widgetName;

-- Insert widgets before or after a target
INSERT BEFORE widgetName { widget_definitions };
INSERT AFTER widgetName { widget_definitions };

-- Remove widgets
DROP WIDGET widgetName1, widgetName2;

-- Replace a widget with new widgets
REPLACE widgetName WITH { widget_definitions };

-- Add a page variable
ADD Variables $name : type = 'expression';

-- Drop a page variable
DROP Variables $name;

Description

Modifies an existing page or snippet in-place without requiring a full CREATE OR REPLACE. This is useful when you need to make targeted changes to a page while preserving the rest of its widget tree, including any unsupported or third-party widget types that cannot be round-tripped through MDL.

ALTER PAGE works directly on the raw BSON widget tree, so it preserves widgets and properties that MDL does not explicitly support.

SET

Sets one or more properties on a named widget. Widget names are assigned during CREATE PAGE and can be discovered with DESCRIBE PAGE.

Supported standard properties: Caption, Label, ButtonStyle, Class, Style, Editable, Visible, Name.

For page-level properties (like Title), omit the ON clause.

For pluggable widget properties, use quoted property names (e.g., 'showLabel').

INSERT BEFORE / INSERT AFTER

Inserts new widgets immediately before or after a named widget within its parent container. The new widgets use the same syntax as in CREATE PAGE.

DROP WIDGET

Removes one or more widgets by name. The widget and all its children are removed from the tree.

REPLACE

Replaces a widget (and its entire subtree) with one or more new widgets.

ADD Variables / DROP Variables

Adds or removes page-level variables. Page variables are typed values with default expressions, available for conditional visibility and dynamic behavior.

Parameters

module.Name
The qualified name of the page or snippet to modify. Must already exist.
widgetName
The name of a widget within the page. Use DESCRIBE PAGE to list widget names.
property
A widget property name. Standard properties are unquoted. Pluggable widget properties use single quotes.
value
The new property value. Strings are quoted. Booleans and enumerations are unquoted.

Examples

Change button caption and style:

ALTER PAGE Sales.Order_Edit {
    SET (Caption = 'Save & Close', ButtonStyle = Success) ON btnSave;
};

Remove an unused widget and add a new field:

ALTER PAGE Sales.Order_Edit {
    DROP WIDGET txtUnused;
    INSERT AFTER txtEmail {
        TEXTBOX txtPhone (Label: 'Phone', Attribute: Phone)
    }
};

Replace a widget with multiple new widgets:

ALTER PAGE Sales.Order_Edit {
    REPLACE txtOldField WITH {
        TEXTBOX txtFirstName (Label: 'First Name', Attribute: FirstName)
        TEXTBOX txtLastName (Label: 'Last Name', Attribute: LastName)
    }
};

Set a page-level property:

ALTER PAGE Sales.Order_Edit {
    SET Title = 'Edit Order Details';
};

Set a pluggable widget property:

ALTER PAGE Sales.Order_Edit {
    SET 'showLabel' = false ON cbStatus;
};

Add and drop page variables:

ALTER PAGE Sales.Order_Edit {
    ADD Variables $showAdvanced : Boolean = 'false';
};

ALTER PAGE Sales.Order_Edit {
    DROP Variables $showAdvanced;
};

Modify a snippet:

ALTER SNIPPET MyModule.NavMenu {
    SET Caption = 'Dashboard' ON btnHome;
    INSERT AFTER btnHome {
        ACTIONBUTTON btnReports (Caption: 'Reports', Action: PAGE MyModule.Reports)
    }
};

Combined operations in a single ALTER:

ALTER PAGE MyModule.Customer_Edit {
    SET Title = 'Customer Details';
    SET (Caption = 'Update', ButtonStyle = Primary) ON btnSave;
    DROP WIDGET txtObsolete;
    INSERT BEFORE txtEmail {
        TEXTBOX txtPhone (Label: 'Phone', Attribute: Phone)
    }
    INSERT AFTER txtEmail {
        COMBOBOX cbCategory (Label: 'Category', Attribute: Category)
    }
};

See Also

CREATE PAGE, CREATE SNIPPET, DESCRIBE PAGE, DROP PAGE

DROP PAGE / SNIPPET

Synopsis

DROP PAGE module.Name

DROP SNIPPET module.Name

Description

Removes a page or snippet from the project. The page or snippet must exist; otherwise an error is raised.

Dropping a page or snippet does not automatically update references to it from microflows (e.g., SHOW PAGE), navigation menus, other pages (SNIPPETCALL), or security rules. Use SHOW IMPACT OF module.Name before dropping to identify all references that will break.

Parameters

module.Name
The qualified name of the page or snippet to drop (Module.PageName or Module.SnippetName).

Examples

Drop a page:

DROP PAGE Sales.Order_Edit;

Drop a snippet:

DROP SNIPPET MyModule.CustomerHeader;

Check impact before dropping:

-- See what references this page
SHOW IMPACT OF Sales.Order_Edit;

-- Then drop if safe
DROP PAGE Sales.Order_Edit;

See Also

CREATE PAGE, CREATE SNIPPET, ALTER PAGE, SHOW IMPACT

CREATE LAYOUT

Synopsis

CREATE LAYOUT module.Name
{
    widget_tree
}

Description

Creates a page layout in the specified module. Layouts define the overall structure of pages – they typically include a header, navigation, content placeholder, and footer. Pages reference a layout via the Layout property.

Layout creation in MDL has limited support. Most Mendix projects use layouts provided by the Atlas UI module (e.g., Atlas_Core.Atlas_Default, Atlas_Core.PopupLayout) rather than creating custom layouts through MDL. For advanced layout customization, use Mendix Studio Pro.

Common Atlas Layouts

These layouts are available in most Mendix projects that include Atlas Core:

LayoutDescription
Atlas_Core.Atlas_DefaultStandard responsive page with sidebar navigation
Atlas_Core.Atlas_TopBarPage with top navigation bar
Atlas_Core.PopupLayoutModal popup dialog
Atlas_Core.Atlas_Default_NativePhoneNative mobile layout

Parameters

module.Name
The qualified name of the layout (Module.LayoutName).

Examples

Reference an existing layout when creating a page:

CREATE PAGE MyModule.Dashboard
(
    Title: 'Dashboard',
    Layout: Atlas_Core.Atlas_Default
)
{
    CONTAINER cntMain {
        DYNAMICTEXT txtWelcome (Attribute: WelcomeMessage)
    }
};

Reference a popup layout for a dialog page:

CREATE PAGE MyModule.ConfirmDelete
(
    Params: { $Item: MyModule.Item },
    Title: 'Confirm Delete',
    Layout: Atlas_Core.PopupLayout
)
{
    DATAVIEW dvItem (DataSource: $Item) {
        DYNAMICTEXT txtMessage (Attribute: Name)
        FOOTER footer1 {
            ACTIONBUTTON btnDelete (Caption: 'Delete', Action: DELETE, ButtonStyle: Danger)
            ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES)
        }
    }
};

See Also

CREATE PAGE, SHOW PAGES

Security Statements

Statements for managing project security: module roles, user roles, entity access rules, microflow and page access, demo users, and project-level security settings.

Mendix security operates at two levels. Module roles define permissions within a single module (entity access, microflow execution, page visibility). User roles aggregate module roles into project-wide identities assigned to end users.

Statements

StatementDescription
CREATE MODULE ROLECreate a role within a module
CREATE USER ROLECreate a project-level user role aggregating module roles
GRANTGrant entity, microflow, page, or nanoflow access to roles
REVOKERemove previously granted access
CREATE DEMO USERCreate a demo user for development and testing
StatementSyntax
Show project securitySHOW PROJECT SECURITY
Show module rolesSHOW MODULE ROLES [IN module]
Show user rolesSHOW USER ROLES
Show demo usersSHOW DEMO USERS
Show access on elementSHOW ACCESS ON MICROFLOW|PAGE module.Name
Show security matrixSHOW SECURITY MATRIX [IN module]
Alter project security levelALTER PROJECT SECURITY LEVEL OFF|PROTOTYPE|PRODUCTION
Toggle demo usersALTER PROJECT SECURITY DEMO USERS ON|OFF
Drop module roleDROP MODULE ROLE module.Role
Drop user roleDROP USER ROLE Name
Drop demo userDROP DEMO USER 'username'
Alter user roleALTER USER ROLE Name ADD|REMOVE MODULE ROLES (module.Role, ...)

CREATE MODULE ROLE

Synopsis

CREATE MODULE ROLE module.RoleName [ DESCRIPTION 'text' ]

Description

Creates a new module role within a module. Module roles are the foundation of Mendix security – they are referenced by entity access rules, microflow/page access grants, and aggregated into project-level user roles.

Each module role belongs to exactly one module. The role name must be unique within the module. By convention, common role names include User, Admin, Viewer, and Manager.

If the module does not exist, the statement returns an error.

Parameters

module.RoleName
Qualified name consisting of the module name and the new role name, separated by a dot. The module must already exist.
DESCRIPTION 'text'
Optional human-readable description of the role’s purpose.

Examples

Create a basic module role:

CREATE MODULE ROLE Shop.Admin;

Create a role with a description:

CREATE MODULE ROLE Shop.Admin DESCRIPTION 'Full administrative access to shop module';

Create multiple roles for a module:

CREATE MODULE ROLE HR.Manager DESCRIPTION 'Can manage employees';
CREATE MODULE ROLE HR.Employee DESCRIPTION 'Self-service access';
CREATE MODULE ROLE HR.Viewer DESCRIPTION 'Read-only access';

See Also

CREATE USER ROLE, GRANT, REVOKE

CREATE USER ROLE

Synopsis

CREATE USER ROLE Name ( module.Role [, ...] ) [ MANAGE ALL ROLES ]

Description

Creates a project-level user role that aggregates one or more module roles. User roles are assigned to end users (either directly or via demo users) and determine the combined set of permissions across all modules.

A user role name must be unique at the project level. It is not qualified with a module name because user roles span multiple modules.

The optional MANAGE ALL ROLES clause grants the user role the ability to manage all other user roles in the application. This is typically reserved for administrator roles.

Parameters

Name
The name of the user role. Not module-qualified. Must be unique among all user roles in the project.
module.Role [, ...]
A comma-separated list of module roles to include. Each module role is specified as a qualified name (Module.RoleName). The module roles must already exist.
MANAGE ALL ROLES
Optional. When specified, users with this role can manage (assign/unassign) all user roles in the application.

Examples

Create an administrator role with management privileges:

CREATE USER ROLE AppAdmin (Shop.Admin, System.Administrator) MANAGE ALL ROLES;

Create a regular user role:

CREATE USER ROLE AppUser (Shop.User, Notifications.Viewer);

Create a role that spans multiple modules:

CREATE USER ROLE SalesManager (
    Sales.Manager,
    Inventory.Viewer,
    Reports.User,
    Administration.User
);

See Also

CREATE MODULE ROLE, GRANT, CREATE DEMO USER

GRANT

Synopsis

-- Entity access
GRANT module.Role ON module.Entity ( rights ) [ WHERE 'xpath' ]

-- Microflow access
GRANT EXECUTE ON MICROFLOW module.Name TO module.Role [, ...]

-- Page access
GRANT VIEW ON PAGE module.Name TO module.Role [, ...]

-- Nanoflow access
GRANT EXECUTE ON NANOFLOW module.Name TO module.Role [, ...]

Description

Grants access rights to module roles. There are four forms of the GRANT statement, each controlling a different kind of access.

Entity Access

The entity access form creates an access rule on an entity for a given module role. The rule specifies which CRUD operations are permitted and optionally restricts visibility with an XPath constraint.

Entity access rules control:

  • CREATE – whether the role can create new instances
  • DELETE – whether the role can delete instances
  • READ – which attributes the role can read
  • WRITE – which attributes the role can modify

For READ and WRITE, use * to include all members (attributes and associations), or specify a parenthesized list of specific attribute names.

The optional WHERE clause accepts an XPath expression that restricts which objects the role can access. The XPath is enclosed in single quotes. Use doubled single quotes to escape single quotes inside the expression.

Microflow Access

The microflow access form grants execute permission on a microflow to one or more module roles. This controls whether the role can trigger the microflow (from pages, other microflows, or REST/web service calls).

Page Access

The page access form grants view permission on a page to one or more module roles. This controls whether the role can open the page.

Nanoflow Access

The nanoflow access form grants execute permission on a nanoflow to one or more module roles.

Parameters

module.Role
The module role receiving the access grant. Must be a qualified name (Module.RoleName).
module.Entity
The target entity for entity access rules.
rights
A comma-separated list of access rights for entity access. Valid values:
  • CREATE – allow creating instances
  • DELETE – allow deleting instances
  • READ * – read all members
  • READ (Attr1, Attr2, ...) – read specific attributes
  • WRITE * – write all members
  • WRITE (Attr1, Attr2, ...) – write specific attributes
WHERE 'xpath'
Optional XPath constraint for entity access. Restricts which objects the rule applies to.
module.Name
The target microflow, nanoflow, or page.
TO module.Role [, ...]
One or more module roles receiving the access (for microflow, page, and nanoflow forms).

Examples

Grant full entity access:

GRANT Shop.Admin ON Shop.Customer (CREATE, DELETE, READ *, WRITE *);

Grant read-only access:

GRANT Shop.Viewer ON Shop.Customer (READ *);

Grant selective attribute access:

GRANT Shop.User ON Shop.Customer (READ (Name, Email), WRITE (Email));

Grant entity access with an XPath constraint:

GRANT Shop.User ON Shop.Order (READ *, WRITE *)
    WHERE '[Status = ''Open'']';

Grant microflow execution to multiple roles:

GRANT EXECUTE ON MICROFLOW Shop.ACT_Order_Process TO Shop.User, Shop.Admin;

Grant page visibility:

GRANT VIEW ON PAGE Shop.Order_Overview TO Shop.User, Shop.Admin;

Grant nanoflow execution:

GRANT EXECUTE ON NANOFLOW Shop.NAV_ValidateInput TO Shop.User;

See Also

REVOKE, CREATE MODULE ROLE, CREATE USER ROLE

REVOKE

Synopsis

-- Entity access
REVOKE module.Role ON module.Entity

-- Microflow access
REVOKE EXECUTE ON MICROFLOW module.Name FROM module.Role [, ...]

-- Page access
REVOKE VIEW ON PAGE module.Name FROM module.Role [, ...]

-- Nanoflow access
REVOKE EXECUTE ON NANOFLOW module.Name FROM module.Role [, ...]

Description

Removes previously granted access rights from module roles. Each form is the counterpart to the corresponding GRANT statement.

Entity Access

Removes the entire entity access rule for the specified module role on the entity. Unlike GRANT, there is no way to partially revoke (e.g., remove only WRITE while keeping READ). The entire rule is removed.

Microflow Access

Removes execute permission on a microflow from one or more module roles.

Page Access

Removes view permission on a page from one or more module roles.

Nanoflow Access

Removes execute permission on a nanoflow from one or more module roles.

Parameters

module.Role
The module role losing access. Must be a qualified name (Module.RoleName).
module.Entity
The entity whose access rule is removed.
module.Name
The target microflow, nanoflow, or page.
FROM module.Role [, ...]
One or more module roles losing access (for microflow, page, and nanoflow forms).

Examples

Remove entity access for a role:

REVOKE Shop.Viewer ON Shop.Customer;

Remove microflow execution from multiple roles:

REVOKE EXECUTE ON MICROFLOW Shop.ACT_Order_Process FROM Shop.Viewer;

Remove page visibility:

REVOKE VIEW ON PAGE Shop.Admin_Dashboard FROM Shop.User;

Remove nanoflow execution:

REVOKE EXECUTE ON NANOFLOW Shop.NAV_ValidateInput FROM Shop.Viewer;

See Also

GRANT, CREATE MODULE ROLE

CREATE DEMO USER

Synopsis

CREATE DEMO USER 'username' PASSWORD 'password'
    [ ENTITY module.Entity ]
    ( UserRole [, ...] )

Description

Creates a demo user for development and testing. Demo users appear on the login screen when running the application locally, allowing quick login without manual credential entry.

Demo users require that project security has demo users enabled (ALTER PROJECT SECURITY DEMO USERS ON).

The optional ENTITY clause specifies which entity (a specialization of System.User) stores the demo user. If omitted, the system auto-detects the unique System.User subtype in the project (typically Administration.Account).

Parameters

'username'
The login name for the demo user. Enclosed in single quotes.
PASSWORD 'password'
The password for the demo user. Enclosed in single quotes.
ENTITY module.Entity
Optional. The entity that generalizes System.User (e.g., Administration.Account). If the project has exactly one System.User subtype, this can be omitted and it will be auto-detected.
UserRole [, ...]
One or more project-level user role names (unqualified) to assign to the demo user.

Examples

Create a demo user with auto-detected entity:

CREATE DEMO USER 'demo_admin' PASSWORD 'Admin123!' (AppAdmin);

Create a demo user with an explicit entity:

CREATE DEMO USER 'demo_admin' PASSWORD 'Admin123!'
    ENTITY Administration.Account (AppAdmin);

Create multiple demo users for different roles:

CREATE DEMO USER 'admin' PASSWORD '1' ENTITY Administration.Account (AppAdmin);
CREATE DEMO USER 'user' PASSWORD '1' ENTITY Administration.Account (AppUser);
CREATE DEMO USER 'viewer' PASSWORD '1' ENTITY Administration.Account (AppViewer);

See Also

CREATE USER ROLE, GRANT

Navigation Statements

Statements for configuring application navigation profiles, home pages, login pages, and menu structures.

Mendix applications can have multiple navigation profiles (Responsive, Tablet, Phone, NativePhone) that each define a home page, optional login page, optional 404 page, and a hierarchical menu tree. Navigation profiles control which page the user sees first and how they navigate through the application.

Statements

StatementDescription
ALTER NAVIGATIONCreate or replace a navigation profile with home pages, login page, and menus
SHOW NAVIGATIONDisplay navigation profiles, menus, and home page assignments
StatementSyntax
Describe navigationDESCRIBE NAVIGATION [profile]

ALTER NAVIGATION

Synopsis

CREATE OR REPLACE NAVIGATION profile
    HOME PAGE module.PageName
    [ HOME PAGE module.PageName FOR module.UserRole ]
    [ LOGIN PAGE module.PageName ]
    [ NOT FOUND PAGE module.PageName ]
    [ MENU (
        menu_items
    ) ]

Description

Creates or replaces a navigation profile. Each profile defines the home page, optional role-specific home pages, optional login page, optional 404 page, and a hierarchical menu structure.

The statement fully replaces the existing profile configuration. To modify only part of a profile’s navigation, use DESCRIBE NAVIGATION to export the current configuration, edit the MDL, and re-execute.

Profile Types

Mendix supports the following navigation profile types:

ProfileDescription
ResponsiveWeb browser (desktop and mobile responsive)
TabletTablet-optimized web
PhonePhone-optimized web
NativePhoneNative mobile application

Menu items form a hierarchy. Top-level items appear in the main navigation bar. Nested submenus are created with the MENU 'label' ( ... ) syntax.

Each MENU ITEM specifies a label and a target page. Menu items are terminated with semicolons.

Parameters

profile
The navigation profile type: Responsive, Tablet, Phone, or NativePhone.
HOME PAGE module.PageName
The default home page for the profile. Required. The page must already exist.
HOME PAGE module.PageName FOR module.UserRole
Optional role-specific home page. Users with this role see a different home page than the default. Multiple role-specific home pages can be specified.
LOGIN PAGE module.PageName
Optional custom login page. If omitted, the default system login page is used.
NOT FOUND PAGE module.PageName
Optional custom 404 page shown when a requested page is not found.
Optional menu structure. Contains MENU ITEM and nested MENU entries.
A leaf menu item that navigates to a page.
A submenu containing nested menu items and/or further submenus.

Examples

Minimal navigation with just a home page:

CREATE OR REPLACE NAVIGATION Responsive
    HOME PAGE MyModule.Home_Web;

Full navigation with role-specific homes and menus:

CREATE OR REPLACE NAVIGATION Responsive
    HOME PAGE MyModule.Home_Web
    HOME PAGE MyModule.AdminHome FOR MyModule.Administrator
    LOGIN PAGE Administration.Login
    NOT FOUND PAGE MyModule.Custom404
    MENU (
        MENU ITEM 'Home' PAGE MyModule.Home_Web;
        MENU 'Admin' (
            MENU ITEM 'Users' PAGE Administration.Account_Overview;
            MENU ITEM 'Settings' PAGE MyModule.Settings;
        );
        MENU ITEM 'About' PAGE MyModule.About;
    );

Native mobile navigation:

CREATE OR REPLACE NAVIGATION NativePhone
    HOME PAGE Mobile.Dashboard
    MENU (
        MENU ITEM 'Home' PAGE Mobile.Dashboard;
        MENU ITEM 'Tasks' PAGE Mobile.TaskList;
        MENU ITEM 'Profile' PAGE Mobile.UserProfile;
    );

See Also

SHOW NAVIGATION

SHOW NAVIGATION

Synopsis

SHOW NAVIGATION
SHOW NAVIGATION MENU [ profile ]
SHOW NAVIGATION HOMES
DESCRIBE NAVIGATION [ profile ]

Description

Displays navigation configuration for the current project.

SHOW NAVIGATION

Displays a summary of all configured navigation profiles, showing the profile type and whether a home page, login page, and menu are configured.

SHOW NAVIGATION MENU

Displays the menu tree for one or all navigation profiles. When a profile is specified, only that profile’s menu is shown. Without a profile, all profiles’ menus are displayed.

The menu tree is rendered as an indented hierarchy showing menu labels and their target pages.

SHOW NAVIGATION HOMES

Displays home page assignments across all navigation profiles, including role-specific overrides.

DESCRIBE NAVIGATION

Outputs the full MDL representation of one or all navigation profiles. The output is round-trippable – it can be re-executed with CREATE OR REPLACE NAVIGATION to recreate the profile.

Parameters

profile
Optional. One of: Responsive, Tablet, Phone, NativePhone. When omitted, all profiles are shown.

Examples

Show a summary of all profiles:

SHOW NAVIGATION;

Show the menu tree for the responsive profile:

SHOW NAVIGATION MENU Responsive;

Show all home page assignments:

SHOW NAVIGATION HOMES;

Export the responsive profile as MDL:

DESCRIBE NAVIGATION Responsive;

See Also

ALTER NAVIGATION

Workflow Statements

Statements for creating, inspecting, and dropping workflows.

Mendix workflows model long-running business processes with user tasks, decisions, parallel splits, and other activity types. A workflow is parameterized by a context entity that carries the data flowing through the process.

Statements

StatementDescription
CREATE WORKFLOWDefine a workflow with activities, user tasks, decisions, and parallel paths
DROP WORKFLOWRemove a workflow from the project
StatementSyntax
Show workflowsSHOW WORKFLOWS [IN module]
Describe workflowDESCRIBE WORKFLOW module.Name
Grant workflow accessGRANT EXECUTE ON WORKFLOW module.Name TO module.Role, ...
Revoke workflow accessREVOKE EXECUTE ON WORKFLOW module.Name FROM module.Role, ...

CREATE WORKFLOW

Synopsis

CREATE [ OR MODIFY ] WORKFLOW module.Name
    PARAMETER $Variable: module.Entity
    [ OVERVIEW PAGE module.PageName ]
    [ DISPLAY 'caption' ]
    [ DESCRIPTION 'text' ]
BEGIN
    activities
END WORKFLOW

Description

Creates a workflow that models a long-running business process. Each workflow has a context parameter (an entity that carries the data through the process) and a sequence of activities.

The OR MODIFY option updates an existing workflow if one exists with the same name, or creates a new one if it does not.

Activity Types

The following activity types can appear inside a workflow body:

USER TASK
An activity that requires human action. A user task displays a page to an assigned user and waits for them to select an outcome. Supports targeting by microflow or XPath to determine the assignee.
USER TASK name 'caption'
    [ PAGE module.PageName ]
    [ TARGETING MICROFLOW module.MicroflowName ]
    [ TARGETING XPATH 'expression' ]
    [ ENTITY module.Entity ]
    [ DUE DATE 'expression' ]
    [ DESCRIPTION 'text' ]
    OUTCOMES 'OutcomeName' { activities } [ 'OutcomeName' { activities } ... ]
MULTI USER TASK
Same as USER TASK but assigned to multiple users. Uses the same syntax with MULTI USER TASK instead of USER TASK.
CALL MICROFLOW
Calls a microflow as part of the workflow. The microflow can return a Boolean or enumeration to drive conditional outcomes.
CALL MICROFLOW module.Name [ 'caption' ]
    [ WITH ( Parameter = expression [, ...] ) ]
    [ OUTCOMES 'OutcomeName' { activities } ... ]
CALL WORKFLOW
Calls a sub-workflow.
CALL WORKFLOW module.Name [ 'caption' ]
    [ WITH ( Parameter = expression [, ...] ) ]
DECISION
A conditional branch based on an expression. Each outcome maps to a different execution path.
DECISION [ 'caption' ]
    OUTCOMES 'OutcomeName' { activities } [ 'OutcomeName' { activities } ... ]
PARALLEL SPLIT
Splits the workflow into parallel paths that execute concurrently and rejoin before continuing.
PARALLEL SPLIT [ 'caption' ]
    PATH 1 { activities }
    PATH 2 { activities }
    [ PATH n { activities } ... ]
JUMP TO
Unconditionally jumps to a named activity earlier in the workflow, creating a loop.
JUMP TO activity_name [ 'caption' ]
WAIT FOR TIMER
Pauses the workflow until a timer expression elapses.
WAIT FOR TIMER [ 'duration_expression' ]
WAIT FOR NOTIFICATION
Pauses the workflow until an external notification resumes it.
WAIT FOR NOTIFICATION
END
Explicitly terminates a workflow path.
END

Boundary Events

User tasks, call microflow activities, and wait-for-notification activities support boundary events that trigger alternative flows when a timer expires:

USER TASK name 'caption'
    PAGE module.Page
    OUTCOMES 'Done' { }
    BOUNDARY EVENT InterruptingTimer '${PT1H}' { activities }
    BOUNDARY EVENT NonInterruptingTimer '${PT30M}' { activities }

Parameters

module.Name
Qualified name of the workflow (Module.WorkflowName).
PARAMETER $Variable: module.Entity
The context parameter. The variable name (prefixed with $) and entity type define the data that flows through the workflow. The entity must already exist.
OVERVIEW PAGE module.PageName
Optional overview page displayed when viewing workflow instances.
DISPLAY 'caption'
Optional display name shown in the workflow overview.
DESCRIPTION 'text'
Optional description of the workflow’s purpose.

Examples

Simple approval workflow:

CREATE WORKFLOW HR.ApprovalFlow
    PARAMETER $Context: HR.LeaveRequest
    OVERVIEW PAGE HR.WorkflowOverview
BEGIN
    USER TASK ReviewTask 'Review the request'
        PAGE HR.ReviewPage
        OUTCOMES 'Approve' { } 'Reject' { };
END WORKFLOW;

Workflow with decision and parallel paths:

CREATE WORKFLOW Sales.OrderProcessing
    PARAMETER $Context: Sales.Order
BEGIN
    CALL MICROFLOW Sales.ACT_ValidateOrder 'Validate'
        OUTCOMES 'True' {
            PARALLEL SPLIT
                PATH 1 {
                    CALL MICROFLOW Sales.ACT_ReserveInventory;
                }
                PATH 2 {
                    CALL MICROFLOW Sales.ACT_ProcessPayment;
                };

            USER TASK ShipTask 'Arrange shipping'
                PAGE Sales.ShippingPage
                TARGETING MICROFLOW Sales.ACT_GetWarehouseStaff
                OUTCOMES 'Shipped' { };
        }
        'False' {
            CALL MICROFLOW Sales.ACT_NotifyCustomer;
        };
END WORKFLOW;

Workflow with a timer boundary event:

CREATE WORKFLOW Support.TicketEscalation
    PARAMETER $Context: Support.Ticket
BEGIN
    USER TASK AssignTask 'Handle ticket'
        PAGE Support.TicketPage
        OUTCOMES 'Resolved' { }
        BOUNDARY EVENT InterruptingTimer '${PT24H}' {
            CALL MICROFLOW Support.ACT_EscalateTicket;
        };
END WORKFLOW;

See Also

DROP WORKFLOW

DROP WORKFLOW

Synopsis

DROP WORKFLOW module.Name

Description

Removes a workflow from the project. The workflow is identified by its qualified name. If the workflow does not exist, an error is returned.

Any references to the dropped workflow (e.g., CALL WORKFLOW activities in other workflows, or navigation entries) will become broken and should be updated or removed.

Parameters

module.Name
The qualified name of the workflow to drop (Module.WorkflowName).

Examples

DROP WORKFLOW HR.ApprovalFlow;

See Also

CREATE WORKFLOW

Business Event Statements

Statements for managing business event services.

Business event services enable asynchronous, event-driven communication between Mendix applications (and external systems) using the Mendix Business Events platform. A service defines named messages with typed attributes and specifies whether the application publishes or subscribes to each message.

Statements

StatementDescription
CREATE BUSINESS EVENT SERVICEDefine a business event service with messages
DROP BUSINESS EVENT SERVICERemove a business event service
StatementSyntax
Show business eventsSHOW BUSINESS EVENTS [IN module]
Describe serviceDESCRIBE BUSINESS EVENT SERVICE module.Name

CREATE BUSINESS EVENT SERVICE

Synopsis

CREATE [ OR REPLACE ] BUSINESS EVENT SERVICE module.Name
(
    ServiceName: 'service_name',
    EventNamePrefix: 'prefix'
)
{
    MESSAGE MessageName ( attr: Type [, ...] ) PUBLISH | SUBSCRIBE
        [ ENTITY module.Entity ] ;
    [ ... ]
}

Description

Creates a business event service that defines one or more messages for event-driven communication between applications.

Each message has a name, a set of typed attributes, and an operation mode (PUBLISH or SUBSCRIBE). A publishing message means this application sends events of that type. A subscribing message means this application receives and handles events of that type.

The OR REPLACE option drops any existing service with the same name before creating the new one.

Service Properties

The service declaration includes two required properties:

  • ServiceName – The logical service name used for event routing on the Mendix Business Events broker.
  • EventNamePrefix – A prefix prepended to message names to form the fully qualified event name. Can be empty ('').

Message Attributes

Each message defines zero or more attributes with the following supported types:

TypeDescription
StringText value
Integer32-bit integer
Long64-bit integer
BooleanTrue/false
DateTimeDate and time
DecimalArbitrary-precision decimal

Entity Mapping

The optional ENTITY clause on a message links the message to a Mendix entity. For SUBSCRIBE messages, incoming events are mapped to instances of this entity. For PUBLISH messages, entity instances are serialized into outgoing events.

Parameters

module.Name
Qualified name of the business event service (Module.ServiceName).
ServiceName: 'service_name'
The logical service identifier used by the Business Events broker.
EventNamePrefix: 'prefix'
A prefix for event names. Use an empty string ('') for no prefix.
MESSAGE MessageName
The name of a message within the service.
attr: Type
An attribute definition within a message. Multiple attributes are comma-separated.
PUBLISH
This application publishes (sends) this message type.
SUBSCRIBE
This application subscribes to (receives) this message type.
ENTITY module.Entity
Optional entity linked to the message for data mapping.

Examples

Create a service that publishes order events:

CREATE BUSINESS EVENT SERVICE Shop.OrderEvents
(
    ServiceName: 'com.example.shop.orders',
    EventNamePrefix: 'shop'
)
{
    MESSAGE OrderCreated (OrderId: Long, CustomerName: String, Total: Decimal) PUBLISH
        ENTITY Shop.Order;
    MESSAGE OrderShipped (OrderId: Long, TrackingNumber: String) PUBLISH
        ENTITY Shop.Shipment;
};

Create a service that subscribes to external events:

CREATE BUSINESS EVENT SERVICE Inventory.StockUpdates
(
    ServiceName: 'com.example.warehouse.stock',
    EventNamePrefix: ''
)
{
    MESSAGE StockChanged (ProductId: Long, NewQuantity: Integer) SUBSCRIBE
        ENTITY Inventory.StockLevel;
};

Replace an existing service:

CREATE OR REPLACE BUSINESS EVENT SERVICE Shop.OrderEvents
(
    ServiceName: 'com.example.shop.orders',
    EventNamePrefix: 'shop'
)
{
    MESSAGE OrderCreated (OrderId: Long, CustomerName: String, Total: Decimal) PUBLISH;
    MESSAGE OrderCancelled (OrderId: Long, Reason: String) PUBLISH;
};

See Also

DROP BUSINESS EVENT SERVICE

DROP BUSINESS EVENT SERVICE

Synopsis

DROP BUSINESS EVENT SERVICE module.Name

Description

Removes a business event service from the project. The service is identified by its qualified name. If the service does not exist, an error is returned.

Dropping a service removes all its message definitions and operation implementations. Any microflows that reference the dropped service’s messages will need to be updated.

Parameters

module.Name
The qualified name of the business event service to drop (Module.ServiceName).

Examples

DROP BUSINESS EVENT SERVICE Shop.OrderEvents;

See Also

CREATE BUSINESS EVENT SERVICE

Image Collection Statements

Statements for creating, inspecting, and dropping image collections within modules.

StatementDescription
CREATE IMAGE COLLECTIONCreate a new image collection with optional images
DROP IMAGE COLLECTIONRemove an image collection
SHOW / DESCRIBE IMAGE COLLECTIONList or inspect image collections

CREATE IMAGE COLLECTION

Synopsis

CREATE IMAGE COLLECTION module.name
    [EXPORT LEVEL 'Hidden' | 'Public']
    [COMMENT 'description']
    [(
        IMAGE 'image_name' FROM FILE 'path',
        ...
    )];

Description

Creates a new image collection in the specified module. Image collections bundle images (icons, logos, graphics) within a module. Images can be loaded from the filesystem during creation, with the format detected automatically from the file extension.

Parameters

module.name
The qualified name of the collection (e.g., MyModule.AppIcons).
EXPORT LEVEL
Controls visibility from other modules. 'Hidden' (default) restricts access to the owning module. 'Public' makes images available to other modules.
COMMENT
Documentation text for the collection.
IMAGE ‘name’ FROM FILE ‘path’
Loads an image from a file on disk. The path is relative to the current working directory. Supported formats: PNG, SVG, GIF, JPEG, BMP, WebP.

Examples

Empty collection

CREATE IMAGE COLLECTION MyModule.AppIcons;

Public collection with description

CREATE IMAGE COLLECTION MyModule.SharedIcons
    EXPORT LEVEL 'Public'
    COMMENT 'Shared icons for all modules';

Collection with images

CREATE IMAGE COLLECTION MyModule.NavigationIcons (
    IMAGE 'home' FROM FILE 'assets/home.png',
    IMAGE 'settings' FROM FILE 'assets/settings.svg',
    IMAGE 'profile' FROM FILE 'assets/profile.png'
);

All options combined

CREATE IMAGE COLLECTION MyModule.BrandAssets
    EXPORT LEVEL 'Public'
    COMMENT 'Company branding assets' (
    IMAGE 'logo-dark' FROM FILE 'assets/logo-dark.png',
    IMAGE 'logo-light' FROM FILE 'assets/logo-light.png'
);

See Also

DROP IMAGE COLLECTION, SHOW / DESCRIBE IMAGE COLLECTION

DROP IMAGE COLLECTION

Synopsis

DROP IMAGE COLLECTION module.name;

Description

Removes an image collection and all its embedded images from the module.

Parameters

module.name
The qualified name of the collection to drop (e.g., MyModule.AppIcons).

Examples

DROP IMAGE COLLECTION MyModule.StatusIcons;

Check references before dropping

SHOW REFERENCES TO MyModule.AppIcons;
DROP IMAGE COLLECTION MyModule.AppIcons;

See Also

CREATE IMAGE COLLECTION, SHOW / DESCRIBE IMAGE COLLECTION

SHOW / DESCRIBE IMAGE COLLECTION

Synopsis

SHOW IMAGE COLLECTION;
SHOW IMAGE COLLECTION IN module;
DESCRIBE IMAGE COLLECTION module.name;

Description

SHOW IMAGE COLLECTION lists all image collections in the project, optionally filtered by module. DESCRIBE IMAGE COLLECTION shows the full definition of a specific collection, including its images as a re-executable CREATE statement.

In the TUI, images are rendered inline when the terminal supports it (Kitty, iTerm2, Sixel).

Parameters

IN module
Filters the listing to a single module.
module.name
The qualified name of the collection to describe (e.g., MyModule.AppIcons).

Examples

List all image collections

SHOW IMAGE COLLECTION;

Filter by module

SHOW IMAGE COLLECTION IN MyModule;

View full definition

DESCRIBE IMAGE COLLECTION MyModule.AppIcons;

The output includes the complete CREATE statement with all IMAGE ... FROM FILE entries, which can be copied and re-executed.

See Also

CREATE IMAGE COLLECTION, DROP IMAGE COLLECTION

Catalog Statements

The catalog is a SQLite database that caches project metadata for fast querying and cross-reference navigation. It is stored in .mxcli/catalog.db next to the MPR file.

A basic REFRESH CATALOG populates tables for modules, entities, attributes, associations, microflows, nanoflows, pages, snippets, enumerations, and workflows. A REFRESH CATALOG FULL additionally populates cross-references (REFS), widgets, strings, source text, and permissions – enabling callers/callees analysis, impact analysis, and full-text search.

StatementDescription
REFRESH CATALOGRebuild the catalog from the current project state
SELECT FROM CATALOGQuery catalog tables with SQL syntax
SHOW CATALOG TABLESList available catalog tables and their columns
SHOW CALLERS / CALLEESFind what calls an element or what it calls
SHOW REFERENCES / IMPACT / CONTEXTCross-reference navigation and impact analysis

REFRESH CATALOG

Synopsis

REFRESH CATALOG [ FULL ] [ FORCE ]

Description

Rebuilds the project metadata catalog from the current state of the open project. The catalog is a SQLite database (.mxcli/catalog.db) that enables fast SQL querying and cross-reference navigation over project elements.

Without any options, REFRESH CATALOG performs a basic rebuild that populates the core metadata tables: MODULES, ENTITIES, ATTRIBUTES, ASSOCIATIONS, MICROFLOWS, NANOFLOWS, PAGES, SNIPPETS, ENUMERATIONS, and WORKFLOWS.

With the FULL option, the rebuild additionally populates cross-reference tables (REFS), widget inventories (WIDGETS), string tables (STRINGS), source text (SOURCE), activity tables (ACTIVITIES), and permission tables (PERMISSIONS). The FULL rebuild is required before using SHOW CALLERS, SHOW CALLEES, SHOW REFERENCES, SHOW IMPACT, SHOW CONTEXT, or SEARCH.

The catalog is cached between sessions. If the project has not changed, repeated REFRESH CATALOG calls reuse the cached data. Use FORCE to bypass the cache and rebuild unconditionally – useful after external changes to the MPR file.

Parameters

FULL
Include cross-references, widgets, strings, source text, and permissions in the catalog. Required for callers/callees analysis and full-text search.
FORCE
Bypass the catalog cache and force a complete rebuild regardless of whether the project appears unchanged.

Examples

Basic catalog refresh

REFRESH CATALOG;

Full rebuild with cross-references

REFRESH CATALOG FULL;

Force a complete rebuild

REFRESH CATALOG FULL FORCE;

Refresh from the command line

-- Shell commands:
-- mxcli -p app.mpr -c "REFRESH CATALOG"
-- mxcli -p app.mpr -c "REFRESH CATALOG FULL"
-- mxcli -p app.mpr -c "REFRESH CATALOG FULL FORCE"

See Also

SELECT FROM CATALOG, SHOW CATALOG TABLES, SHOW CALLERS / CALLEES

SELECT FROM CATALOG

Synopsis

SELECT columns FROM CATALOG.table [ WHERE condition ] [ ORDER BY column [ ASC | DESC ] ] [ LIMIT n ]

Description

Executes a SQL query against the project metadata catalog. The catalog tables are populated by REFRESH CATALOG and can be queried using standard SQL syntax including joins, aggregations, subqueries, and filtering.

All catalog table names are prefixed with CATALOG. (e.g., CATALOG.ENTITIES, CATALOG.MICROFLOWS). The query engine is SQLite, so standard SQLite SQL syntax applies.

The following tables are available after a basic REFRESH CATALOG:

TableDescription
CATALOG.MODULESProject modules
CATALOG.ENTITIESEntities across all modules
CATALOG.ATTRIBUTESEntity attributes
CATALOG.ASSOCIATIONSAssociations between entities
CATALOG.MICROFLOWSMicroflows
CATALOG.NANOFLOWSNanoflows
CATALOG.PAGESPages
CATALOG.SNIPPETSSnippets
CATALOG.ENUMERATIONSEnumerations
CATALOG.WORKFLOWSWorkflows

The following tables require REFRESH CATALOG FULL:

TableDescription
CATALOG.ACTIVITIESIndividual microflow/nanoflow activities
CATALOG.WIDGETSWidget instances across pages and snippets
CATALOG.REFSCross-references between elements
CATALOG.PERMISSIONSAccess rules and role permissions
CATALOG.STRINGSAll string values in the project
CATALOG.SOURCESource text of microflows, pages, etc.

Parameters

columns
Column names or expressions to select. Use * for all columns, or specify individual column names. Supports SQL functions like COUNT(), SUM(), GROUP_CONCAT().
table
A catalog table name (e.g., ENTITIES, MICROFLOWS). Must be prefixed with CATALOG..
condition
A SQL WHERE clause for filtering rows. Supports standard operators (=, LIKE, IN, AND, OR) and subqueries.
column
Column to sort by. Append ASC (default) or DESC for sort direction.
n
Maximum number of rows to return.

Examples

List all entities

SELECT Name, Module FROM CATALOG.ENTITIES;

Find microflows in a specific module

SELECT Name FROM CATALOG.MICROFLOWS WHERE Module = 'Sales' ORDER BY Name;

Count entities per module

SELECT Module, COUNT(*) AS EntityCount
FROM CATALOG.ENTITIES
GROUP BY Module
ORDER BY EntityCount DESC;

Find entities with many attributes

SELECT e.Module, e.Name, COUNT(a.Name) AS AttrCount
FROM CATALOG.ENTITIES e
JOIN CATALOG.ATTRIBUTES a ON e.Id = a.EntityId
GROUP BY e.Module, e.Name
HAVING COUNT(a.Name) > 10
ORDER BY AttrCount DESC;

Find many-to-many associations

SELECT Name, ParentEntity, ChildEntity
FROM CATALOG.ASSOCIATIONS
WHERE Type = 'ReferenceSet';

Find pages with no microflow references

SELECT p.Module, p.Name
FROM CATALOG.PAGES p
WHERE p.Id NOT IN (
  SELECT DISTINCT TargetId FROM CATALOG.REFS WHERE RefKind = 'page'
);

Limit results

SELECT Name FROM CATALOG.MICROFLOWS LIMIT 10;

See Also

REFRESH CATALOG, SHOW CATALOG TABLES, SEARCH

SHOW CALLERS / CALLEES

Synopsis

SHOW CALLERS OF qualified_name [ TRANSITIVE ]

SHOW CALLEES OF qualified_name [ TRANSITIVE ]

Description

Displays the call relationships for a given element. SHOW CALLERS finds all elements that call or reference the specified element (incoming edges). SHOW CALLEES finds all elements that the specified element calls or references (outgoing edges).

These commands require REFRESH CATALOG FULL to have been run beforehand. Without it, the cross-reference data needed for caller/callee analysis is not available.

By default, only direct (one-hop) relationships are shown. With the TRANSITIVE option, the command follows the call chain recursively to show the full transitive closure – all indirect callers or callees at any depth.

Parameters

qualified_name
The fully qualified name of the element to analyze (e.g., Module.MicroflowName, Module.EntityName). Works with microflows, nanoflows, pages, snippets, entities, and other referenceable elements.
TRANSITIVE
Follow the call chain recursively. For SHOW CALLERS, this finds everything that directly or indirectly leads to the target. For SHOW CALLEES, this finds everything the target directly or indirectly depends on.

Examples

Find direct callers of a microflow

REFRESH CATALOG FULL;
SHOW CALLERS OF Sales.ACT_CreateOrder;

Find all transitive callers

SHOW CALLERS OF Sales.ACT_CreateOrder TRANSITIVE;

Find what a microflow calls

SHOW CALLEES OF Sales.ACT_ProcessOrder;

Find all transitive callees

SHOW CALLEES OF Sales.ACT_ProcessOrder TRANSITIVE;

From the command line

-- Shell commands:
-- mxcli callers -p app.mpr Sales.ACT_CreateOrder
-- mxcli callers -p app.mpr Sales.ACT_CreateOrder --transitive
-- mxcli callees -p app.mpr Sales.ACT_ProcessOrder

See Also

SHOW REFERENCES / IMPACT / CONTEXT, REFRESH CATALOG, SELECT FROM CATALOG

SHOW REFERENCES / IMPACT / CONTEXT

Synopsis

SHOW REFERENCES TO qualified_name

SHOW IMPACT OF qualified_name

SHOW CONTEXT OF qualified_name [ DEPTH n ]

Description

These commands provide different views of cross-reference information for a given element. All three require REFRESH CATALOG FULL to have been run beforehand.

SHOW REFERENCES TO lists all elements that reference the specified element. This includes microflows that use an entity, pages that display it, associations that connect to it, and any other form of reference.

SHOW IMPACT OF performs an impact analysis showing what would be affected if the specified element were changed or removed. This is broader than SHOW REFERENCES as it considers transitive dependencies and indirect effects.

SHOW CONTEXT OF assembles the surrounding context of an element – its definition, its callers, callees, and related elements – suitable for providing to an LLM or for understanding an element in its broader project context. The optional DEPTH parameter controls how many levels of related elements to include.

Parameters

qualified_name
The fully qualified name of the element to analyze (e.g., Module.EntityName, Module.MicroflowName).
n (CONTEXT only)
The number of levels of related elements to include. Defaults to 1 if not specified. Higher values include more surrounding context but produce more output.

Examples

Find all references to an entity

REFRESH CATALOG FULL;
SHOW REFERENCES TO Sales.Customer;

Analyze impact before making changes

SHOW IMPACT OF Sales.Customer;

Gather context for a microflow

SHOW CONTEXT OF Sales.ACT_CreateOrder;

Gather deeper context

SHOW CONTEXT OF Sales.ACT_CreateOrder DEPTH 3;

Check impact before moving an element

SHOW IMPACT OF Sales.CustomerEdit;
MOVE PAGE Sales.CustomerEdit TO NewModule;

From the command line

-- Shell commands:
-- mxcli refs -p app.mpr Sales.Customer
-- mxcli impact -p app.mpr Sales.Customer
-- mxcli context -p app.mpr Sales.ACT_CreateOrder --depth 3

See Also

SHOW CALLERS / CALLEES, REFRESH CATALOG, SELECT FROM CATALOG

SHOW CATALOG TABLES

Synopsis

SHOW CATALOG TABLES

Description

Lists all available catalog tables along with their column names and types. This is useful for discovering what metadata is queryable before writing SELECT FROM CATALOG queries.

The output includes both basic tables (populated by REFRESH CATALOG) and extended tables (populated by REFRESH CATALOG FULL). Tables that require a full refresh are marked accordingly.

Parameters

This statement takes no parameters.

Examples

List all catalog tables

SHOW CATALOG TABLES;

Typical workflow: discover then query

REFRESH CATALOG;
SHOW CATALOG TABLES;
SELECT * FROM CATALOG.ENTITIES LIMIT 5;

See Also

REFRESH CATALOG, SELECT FROM CATALOG

External SQL Statements

Statements for connecting to and querying external databases (PostgreSQL, Oracle, SQL Server). Credentials are isolated from session output and logs – the DSN is never displayed after the initial connection.

External SQL connections are independent of the Mendix project connection. You can have multiple external database connections open simultaneously, each identified by an alias.

StatementDescription
SQL CONNECTOpen a connection to an external database
SQL DISCONNECTClose an external database connection
SQL (query)Execute SQL queries and schema discovery commands
SQL GENERATE CONNECTORGenerate Database Connector MDL from an external schema
IMPORT FROMImport data from an external database into a Mendix app database

SQL CONNECT

Synopsis

SQL CONNECT driver 'dsn' AS alias

Description

Opens a connection to an external database. The connection is identified by the alias, which is used in all subsequent SQL commands to route queries to the correct database.

Multiple connections can be open simultaneously with different aliases. Use SQL CONNECTIONS to list active connections (shows alias and driver only – the DSN is never displayed for security).

Supported database drivers:

DriverAliasesExample DSN
postgrespg, postgresqlpostgres://user:pass@host:5432/dbname
oracleoraoracle://user:pass@host:1521/service
sqlservermssqlsqlserver://user:pass@host:1433?database=dbname

Parameters

driver
The database driver to use. One of postgres (or pg, postgresql), oracle (or ora), or sqlserver (or mssql).
dsn
The data source name (connection string) enclosed in single quotes. The format depends on the driver. The DSN is stored in memory only and never appears in session output or logs.
alias
A short identifier for this connection. Used in all subsequent SQL alias ... commands. Must be unique among active connections.

Examples

Connect to PostgreSQL

SQL CONNECT postgres 'postgres://user:pass@localhost:5432/mydb' AS source;

Connect to Oracle

SQL CONNECT oracle 'oracle://scott:tiger@dbhost:1521/ORCL' AS erp;

Connect to SQL Server

SQL CONNECT sqlserver 'sqlserver://sa:Password1@localhost:1433?database=northwind' AS legacy;

List active connections

SQL CONNECTIONS;

Connect using driver aliases

SQL CONNECT pg 'postgres://user:pass@localhost:5432/mydb' AS src;
SQL CONNECT mssql 'sqlserver://sa:Pass@host:1433?database=db' AS dst;

See Also

SQL DISCONNECT, SQL (query), SQL GENERATE CONNECTOR

SQL DISCONNECT

Synopsis

SQL DISCONNECT alias

Description

Closes an external database connection identified by the given alias. After disconnecting, the alias is no longer valid and any subsequent SQL alias ... commands will fail.

Use SQL CONNECTIONS to see which connections are currently active before disconnecting.

Parameters

alias
The alias of the connection to close. Must match an active connection established with SQL CONNECT.

Examples

Disconnect a connection

SQL DISCONNECT source;

Full connection lifecycle

SQL CONNECT postgres 'postgres://user:pass@localhost:5432/mydb' AS source;
SQL source SHOW TABLES;
SQL source SELECT COUNT(*) FROM users;
SQL DISCONNECT source;

See Also

SQL CONNECT, SQL (query)

SQL (query)

Synopsis

SQL alias query_text

SQL alias SHOW TABLES
SQL alias SHOW VIEWS
SQL alias SHOW FUNCTIONS
SQL alias DESCRIBE table_name

Description

Executes a SQL query or schema discovery command against an external database connection. The alias identifies which connection to use (established with SQL CONNECT).

Raw SQL passthrough: Any SQL text after the alias is sent directly to the external database. The results are displayed as a formatted table. This supports SELECT, INSERT, UPDATE, DELETE, DDL, and any other SQL the target database accepts.

Schema discovery commands provide a portable way to explore the database schema without writing database-specific SQL:

CommandDescription
SHOW TABLESLists user tables in the database
SHOW VIEWSLists user views in the database
SHOW FUNCTIONSLists functions and stored procedures
DESCRIBE table_nameShows columns, data types, and nullability for a table

The schema discovery commands query information_schema internally and work consistently across PostgreSQL, Oracle, and SQL Server.

Parameters

alias
The connection alias established with SQL CONNECT.
query_text
Any valid SQL for the target database. Sent as-is to the database engine.
table_name (DESCRIBE only)
The name of the table or view to describe.

Examples

Query data

SQL source SELECT * FROM users WHERE active = true LIMIT 10;

List tables

SQL source SHOW TABLES;

List views

SQL source SHOW VIEWS;

Describe a table

SQL source DESCRIBE users;

Insert data

SQL source INSERT INTO audit_log (action, timestamp) VALUES ('export', NOW());

Run aggregate queries

SQL source SELECT department, COUNT(*) AS headcount
  FROM employees
  GROUP BY department
  ORDER BY headcount DESC;

From the command line

-- Shell command:
-- mxcli sql --driver postgres --dsn 'postgres://...' "SELECT * FROM users"

See Also

SQL CONNECT, SQL DISCONNECT, SQL GENERATE CONNECTOR, IMPORT FROM

SQL GENERATE CONNECTOR

Synopsis

SQL alias GENERATE CONNECTOR INTO module [ TABLES ( table [, ...] ) ] [ VIEWS ( view [, ...] ) ] [ EXEC ]

Description

Generates MDL statements that create a Mendix Database Connector module from the schema of an external database. The generated MDL includes entities, attributes, and associations that mirror the external database tables and views, configured for use with the Mendix Database Connector module.

Without EXEC, the command outputs the generated MDL to the console for review. With EXEC, it executes the MDL immediately against the open project.

By default, all user tables are included. Use TABLES (...) and VIEWS (...) to limit generation to specific tables and views.

The type mapping from SQL types to Mendix types is handled automatically (e.g., VARCHAR becomes String, INTEGER becomes Integer, TIMESTAMP becomes DateTime).

Parameters

alias
The connection alias established with SQL CONNECT.
module
The target Mendix module where the connector entities will be created.
table
One or more table names to include. If omitted, all user tables are included.
view
One or more view names to include. If omitted, no views are included unless TABLES is also omitted (in which case all tables are included by default).
EXEC
Execute the generated MDL immediately instead of printing it. Requires an open Mendix project.

Examples

Preview generated MDL for all tables

SQL source GENERATE CONNECTOR INTO HRModule;

Generate for specific tables

SQL source GENERATE CONNECTOR INTO HRModule TABLES (employees, departments);

Generate for tables and views

SQL source GENERATE CONNECTOR INTO HRModule TABLES (employees) VIEWS (employee_summary);

Generate and execute immediately

SQL source GENERATE CONNECTOR INTO HRModule TABLES (employees, departments) EXEC;

Full workflow

SQL CONNECT postgres 'postgres://user:pass@localhost:5432/hrdb' AS source;
SQL source SHOW TABLES;
SQL source DESCRIBE employees;
SQL source GENERATE CONNECTOR INTO HRModule TABLES (employees, departments) EXEC;
SQL DISCONNECT source;

See Also

SQL CONNECT, SQL (query), IMPORT FROM

IMPORT FROM

Synopsis

IMPORT FROM alias QUERY 'sql'
  INTO module.Entity
  MAP ( source_col AS AttrName [, ...] )
  [ LINK ( source_col TO AssocName ON MatchAttr ) [, ...] ]
  [ BATCH size ]
  [ LIMIT count ]

Description

Imports data from an external database into a Mendix application’s PostgreSQL database. The command executes the specified SQL query against the external connection, maps the result columns to entity attributes, and inserts rows in batches using direct database insertion.

The import pipeline handles:

  • Mendix ID generation: Automatically generates unique Mendix object IDs for each inserted row, following the Mendix ID format and sequence tracking.
  • Batch insertion: Rows are inserted in configurable batch sizes for performance.
  • Association linking: The LINK clause matches imported rows to existing objects via an association, looking up the target by a matching attribute value.

The target Mendix application database is auto-detected from the project’s database configuration. The import connects directly to the application’s PostgreSQL database.

Parameters

alias
The external database connection alias established with SQL CONNECT.
sql
A SQL query string (in single quotes) to execute against the external database. The result set columns are available for mapping.
module.Entity
The fully qualified Mendix entity to import into (e.g., HR.Employee).
source_col
A column name from the SQL query result set.
AttrName
The target attribute name on the Mendix entity.
The name of the association to set. The source column value is matched against the MatchAttr on the associated entity to find the target object.
The attribute on the associated entity used to look up the target object for linking.
size (BATCH only)
Number of rows per insert batch. Defaults to a reasonable batch size if not specified.
count (LIMIT only)
Maximum number of rows to import from the query result.

Examples

Basic import

IMPORT FROM source QUERY 'SELECT name, email FROM employees'
  INTO HR.Employee
  MAP (name AS Name, email AS Email);

Import with association linking

IMPORT FROM source QUERY 'SELECT name, email, dept_name FROM employees'
  INTO HR.Employee
  MAP (name AS Name, email AS Email)
  LINK (dept_name TO Employee_Department ON Name);

Import with batch size and limit

IMPORT FROM source QUERY 'SELECT name, email, dept_name FROM employees'
  INTO HR.Employee
  MAP (name AS Name, email AS Email)
  LINK (dept_name TO Employee_Department ON Name)
  BATCH 500
  LIMIT 1000;

Full workflow: connect, explore, import

SQL CONNECT postgres 'postgres://user:pass@legacydb:5432/hr' AS source;
SQL source SHOW TABLES;
SQL source DESCRIBE employees;

IMPORT FROM source QUERY 'SELECT name, email FROM employees WHERE active = true'
  INTO HR.Employee
  MAP (name AS Name, email AS Email)
  BATCH 500;

SQL DISCONNECT source;

See Also

SQL CONNECT, SQL (query), SQL GENERATE CONNECTOR

Settings Statements

Statements for viewing and modifying Mendix project settings. Settings are organized into categories: MODEL (application-level settings), CONFIGURATION (runtime configurations), CONSTANT (constant value overrides per configuration), LANGUAGE (localization settings), and WORKFLOWS (workflow engine settings).

StatementDescription
SHOW / DESCRIBE SETTINGSView current project settings
ALTER SETTINGSModify project settings by category

SHOW / DESCRIBE SETTINGS

Synopsis

SHOW SETTINGS

DESCRIBE SETTINGS [ category ]

Description

Displays project settings. SHOW SETTINGS provides a compact overview of all settings across all categories. DESCRIBE SETTINGS outputs the full settings in round-trippable MDL syntax that can be used to recreate the same configuration.

When DESCRIBE SETTINGS is called without a category, it outputs all settings. When a category is specified, only that category is shown.

The available settings categories are:

CategoryContents
MODELApplication-level settings: AfterStartupMicroflow, BeforeShutdownMicroflow, HashAlgorithm, JavaVersion, etc.
CONFIGURATIONRuntime configurations: DatabaseType, DatabaseUrl, HttpPortNumber, etc. Each named configuration is listed separately.
CONSTANTConstant value overrides per configuration. Shows which constants have non-default values in each configuration.
LANGUAGELocalization settings: DefaultLanguageCode and available languages.
WORKFLOWSWorkflow engine settings: UserEntity, DefaultTaskParallelism, etc.

Parameters

category (DESCRIBE only)
One of MODEL, CONFIGURATION, CONSTANT, LANGUAGE, or WORKFLOWS. If omitted, all categories are shown.

Examples

Show settings overview

SHOW SETTINGS;

Describe all settings in MDL format

DESCRIBE SETTINGS;

Describe model settings only

DESCRIBE SETTINGS MODEL;

Describe runtime configurations

DESCRIBE SETTINGS CONFIGURATION;

Describe workflow settings

DESCRIBE SETTINGS WORKFLOWS;

See Also

ALTER SETTINGS

ALTER SETTINGS

Synopsis

ALTER SETTINGS MODEL key = value

ALTER SETTINGS CONFIGURATION 'name' key = value

ALTER SETTINGS CONSTANT 'name' VALUE 'value' IN CONFIGURATION 'config'

ALTER SETTINGS LANGUAGE key = value

ALTER SETTINGS WORKFLOWS key = value

Description

Modifies project settings by category. Each category has its own syntax and available keys.

MODEL settings control application-level behavior such as the after-startup microflow, hashing algorithm, and Java version.

CONFIGURATION settings control named runtime configurations. Each project can have multiple configurations (e.g., default, staging, production). Settings include database type, database URL, HTTP port number, and other runtime parameters. The configuration name must be enclosed in single quotes.

CONSTANT settings override the default value of a project constant within a specific runtime configuration. Both the constant name and the configuration name must be enclosed in single quotes.

LANGUAGE settings control localization, primarily the default language code.

WORKFLOWS settings control the workflow engine, including the user entity used for workflow tasks and default task parallelism.

Parameters

key
The setting name to modify. Available keys depend on the category.
value
The new value for the setting. String values must be enclosed in single quotes.
name (CONFIGURATION, CONSTANT)
The name of the configuration or constant, enclosed in single quotes.
config (CONSTANT only)
The name of the runtime configuration where the constant override applies, enclosed in single quotes.

Examples

Set the after-startup microflow

ALTER SETTINGS MODEL AfterStartupMicroflow = 'MyModule.ACT_Startup';

Configure database type

ALTER SETTINGS CONFIGURATION 'default' DatabaseType = 'POSTGRESQL';

Set database URL for a configuration

ALTER SETTINGS CONFIGURATION 'production' DatabaseUrl = 'jdbc:postgresql://dbhost:5432/myapp';

Override a constant in a configuration

ALTER SETTINGS CONSTANT 'MyModule.ApiBaseUrl' VALUE 'https://api.staging.example.com' IN CONFIGURATION 'staging';

Set the default language

ALTER SETTINGS LANGUAGE DefaultLanguageCode = 'en_US';

Configure workflow user entity

ALTER SETTINGS WORKFLOWS UserEntity = 'Administration.Account';

Set Java version

ALTER SETTINGS MODEL JavaVersion = '17';

See Also

SHOW / DESCRIBE SETTINGS

Organization Statements

Statements for organizing project structure: creating modules and folders, and moving documents between them.

StatementDescription
CREATE MODULECreate a new module in the project
CREATE FOLDERCreate a folder within a module for organizing documents
MOVEMove a document to a different module or folder

CREATE MODULE

Synopsis

CREATE MODULE module_name

Description

Creates a new module in the Mendix project. Modules are the top-level organizational unit for all project documents including entities, microflows, pages, and enumerations. The module is created with default settings and an empty domain model.

Parameters

module_name
The name of the module to create. Must be a valid identifier (letters, digits, underscores; cannot start with a digit). If the name conflicts with a reserved keyword, enclose it in double quotes.

Examples

Create a module

CREATE MODULE OrderManagement;

Create multiple modules for a layered architecture

CREATE MODULE Sales;
CREATE MODULE Inventory;
CREATE MODULE Reporting;

See Also

CREATE FOLDER, MOVE, DROP MODULE

CREATE FOLDER

Synopsis

CREATE FOLDER module_name/folder_path

Description

Creates a folder within a module for organizing documents such as pages, microflows, and snippets. Nested folders use the / separator. If intermediate folders in the path do not exist, they are created automatically.

Note: Folders can also be created implicitly by specifying a FOLDER clause when creating a microflow or a Folder property when creating a page.

Parameters

module_name
The name of the module in which to create the folder.
folder_path
The path of the folder to create within the module. Use / to create nested folders (e.g., Orders/Processing).

Examples

Create a simple folder

CREATE FOLDER MyModule/Pages;

Create a nested folder

CREATE FOLDER MyModule/Orders/Processing;

Use a folder when creating a microflow

CREATE MICROFLOW MyModule.ACT_ProcessOrder
FOLDER 'Orders/Processing'
BEGIN
  RETURN true;
END;

Use a folder when creating a page

CREATE PAGE MyModule.Order_Edit
(
  Title: 'Edit Order',
  Layout: Atlas_Core.PopupLayout,
  Folder: 'Orders'
)
{
  CONTAINER main () {}
}

See Also

CREATE MODULE, MOVE

MOVE

Synopsis

MOVE document_type qualified_name TO module_name
MOVE document_type qualified_name TO FOLDER 'folder_path'
MOVE document_type qualified_name TO FOLDER 'folder_path' IN module_name

Description

Moves a document to a different folder or module. Supported document types are PAGE, MICROFLOW, NANOFLOW, SNIPPET, ENUMERATION, and ENTITY. When moving to a folder, missing intermediate folders are created automatically. Cross-module moves change the qualified name of the document, which may break by-name references elsewhere in the project.

Entity moves only support moving to a module (not to a folder), because entities are embedded in domain model documents.

Parameters

document_type
The type of document to move. One of: PAGE, MICROFLOW, NANOFLOW, SNIPPET, ENUMERATION, ENTITY.
qualified_name
The current Module.Name of the document to move.
module_name
The target module. When specified without FOLDER, moves the document to the module root.
folder_path
The target folder path within the module. Use / for nested paths (e.g., 'Orders/Processing'). Enclosed in single quotes.

Examples

Move a page to a folder in the same module

MOVE PAGE MyModule.CustomerEdit TO FOLDER 'Customers';

Move a microflow to a nested folder

MOVE MICROFLOW MyModule.ACT_ProcessOrder TO FOLDER 'Orders/Processing';

Move a snippet to a different module

MOVE SNIPPET OldModule.NavigationMenu TO Common;

Move an entity to a different module

MOVE ENTITY OldModule.Customer TO NewModule;

Move a page to a folder in a different module

MOVE PAGE OldModule.CustomerPage TO FOLDER 'Screens' IN NewModule;

Check impact before a cross-module move

SHOW IMPACT OF OldModule.Customer;
MOVE ENTITY OldModule.Customer TO NewModule;

See Also

CREATE MODULE, CREATE FOLDER

Session Statements

Statements for inspecting and configuring the current REPL session.

StatementDescription
SETSet a session variable
SHOW STATUSDisplay the current connection and session information

SET

Synopsis

SET variable = value

Description

Sets a session variable that controls REPL behavior. Session variables persist for the duration of the session and are reset when the REPL exits. Variable names are case-insensitive.

Parameters

variable
The name of the session variable to set.
value
The value to assign. Can be a string (in single quotes), a boolean (TRUE or FALSE), or an unquoted identifier.

Recognized Variables

VariableValuesDescription
output_format / FORMAT'json', 'table'Controls output format for query results
verboseTRUE, FALSEEnable verbose output for debugging
AUTOCOMMITTRUE, FALSEAutomatically save changes after each mutation

Examples

Set output format to JSON

SET output_format = 'json';

Enable verbose mode

SET verbose = TRUE;

Enable autocommit

SET AUTOCOMMIT = TRUE;

See Also

SHOW STATUS, OPEN PROJECT

SHOW STATUS

Synopsis

STATUS

Description

Displays the current session status including the connection state, the open project path, the Mendix version of the project, the MPR format version, and the values of any session variables. This is useful for confirming which project is loaded and verifying session configuration.

Parameters

This statement takes no parameters.

Examples

Check session status

STATUS;

Sample output:

Status: Connected
Project: /Users/dev/projects/MyApp/MyApp.mpr
Version: 10.6.0
Format:  v2
Modules: 5

Verify connection before running commands

STATUS;
SHOW MODULES;

See Also

OPEN PROJECT, CLOSE PROJECT, SET

System Architecture

ModelSDK Go is a Go-native library for reading and modifying Mendix application projects (.mpr files). It provides programmatic access to Mendix projects without cloud connectivity, serving as an alternative to the TypeScript-based Mendix Model SDK.

High-Level Architecture

The system is organized into layered components, from user-facing interfaces down to file system storage:

graph TB
    subgraph "User Interface"
        CLI[mxcli CLI]
        API[Go API]
        VSCODE[VS Code Extension]
    end

    subgraph "MDL Layer"
        REPL[REPL Interface]
        LSP[LSP Server]
        EXEC[Executor]
        PARSER[ANTLR4 Parser]
        AST[AST Nodes]
        VISITOR[AST Visitor]
        CATALOG[Catalog]
        LINTER[Linter]
    end

    subgraph "High-Level API"
        HLAPI[api/]
        DMAPI[DomainModelsAPI]
        MFAPI[MicroflowsAPI]
        PGAPI[PagesAPI]
        ENUMAPI[EnumerationsAPI]
        MODAPI[ModulesAPI]
    end

    subgraph "SDK Layer"
        SDK[modelsdk.go]
        DM[domainmodel]
        MF[microflows]
        PG[pages]
        MODEL[model]
        WIDGETS[widgets]
    end

    subgraph "External SQL"
        SQLPKG[sql/]
        SQLCONN[Connection Manager]
        SQLQUERY[Query Engine]
        SQLIMPORT[Import Pipeline]
        SQLGEN[Connector Generator]
    end

    subgraph "Storage Layer"
        READER[MPR Reader]
        WRITER[MPR Writer]
        BSONP[BSON Parser]
    end

    subgraph "File System"
        MPR[(MPR File)]
        CONTENTS[(mprcontents/)]
        EXTDB[(External DBs)]
    end

    CLI --> REPL
    CLI --> LSP
    API --> HLAPI
    API --> SDK
    VSCODE --> LSP
    HLAPI --> SDK
    HLAPI --> READER
    HLAPI --> WRITER

    REPL --> EXEC
    LSP --> EXEC
    EXEC --> PARSER
    PARSER --> AST
    AST --> VISITOR
    VISITOR --> AST
    EXEC --> SDK
    EXEC --> SQLPKG
    EXEC --> CATALOG
    EXEC --> LINTER

    SDK --> DM
    SDK --> MF
    SDK --> PG
    SDK --> MODEL
    SDK --> WIDGETS
    SDK --> READER
    SDK --> WRITER

    SQLPKG --> SQLCONN
    SQLPKG --> SQLQUERY
    SQLPKG --> SQLIMPORT
    SQLPKG --> SQLGEN
    SQLCONN --> EXTDB

    READER --> BSONP
    WRITER --> BSONP
    READER --> MPR
    READER --> CONTENTS
    WRITER --> MPR
    WRITER --> CONTENTS

Layer Summary

LayerPurpose
User InterfaceCLI (mxcli), Go API, and VS Code extension provide entry points for users and tools
MDL LayerSQL-like language parser, executor, REPL, LSP server, catalog, and linter
High-Level APIFluent builder API for domain models, microflows, pages, enumerations, and modules
SDK LayerCore Go types and operations for Mendix model elements
External SQLDatabase connectivity for PostgreSQL, Oracle, and SQL Server
Storage LayerMPR file reading, writing, and BSON parsing
File SystemPhysical .mpr files, mprcontents/ directories, and external databases

Key Design Decisions

  1. ANTLR4 for Grammar – Enables cross-language grammar sharing (Go, TypeScript, Java) with case-insensitive keywords and built-in error recovery.

  2. BSON for Serialization – Native Mendix format compatibility using go.mongodb.org/mongo-driver/bson, handling polymorphic types via $Type fields.

  3. Two-Phase Loading – Units are loaded on-demand for performance, with lazy loading of related documents.

  4. Interface-Based DesignElement and AttributeType interfaces enable type-safe polymorphic operations across element types.

  5. Pure Go / No CGO – Uses modernc.org/sqlite (pure Go SQLite) to eliminate the C compiler dependency, simplifying cross-compilation and deployment.

  6. Widget Template System – Pluggable widgets require complex BSON with internal ID references. Embedded JSON templates extracted from Studio Pro are cloned and customized at runtime.

  7. Credential Isolation – External database credentials resolve from environment variables or YAML config, never stored in MDL scripts.

Layer Diagram

Detailed description of each architectural layer in ModelSDK Go.

1. Command Layer (cmd/)

PackagePurpose
cmd/mxcliCLI entry point using Cobra framework; includes LSP server, Docker integration, diagnostics
cmd/codegenMetamodel code generator from reflection data

Key CLI subcommands:

SubcommandFilePurpose
execcmd_exec.goExecute MDL script files
checkcmd_check.goSyntax and reference validation
lintcmd_lint.goRun linting rules
reportcmd_report.goBest practices report
testcmd_test_run.goRun .test.mdl / .test.md tests
diffcmd_diff.goCompare script against project
sqlcmd_sql.goExternal SQL queries
lsplsp.goLanguage Server Protocol server
initinit.goProject initialization
dockerdocker.goDocker build/check/OQL integration
diagdiag.goSession logs, bug report bundles

2. MDL Layer (mdl/)

The MDL (Mendix Definition Language) layer provides a SQL-like interface for querying and modifying Mendix models.

sequenceDiagram
    participant User
    participant REPL
    participant Parser
    participant Visitor
    participant Executor
    participant SDK

    User->>REPL: "SHOW ENTITIES IN MyModule"
    REPL->>Parser: Parse MDL command
    Parser->>Visitor: Walk parse tree
    Visitor->>Executor: Build AST node
    Executor->>SDK: ListDomainModels()
    SDK-->>Executor: []*DomainModel
    Executor-->>REPL: Formatted output
    REPL-->>User: Display table
PackagePurpose
mdl/grammarANTLR4 lexer/parser (generated from MDLLexer.g4 + MDLParser.g4)
mdl/astAST node types for MDL statements
mdl/visitorANTLR listener that builds AST from parse tree
mdl/executorExecutes AST nodes against the SDK (~45k lines across 40+ files)
mdl/catalogSQLite-based catalog for querying project metadata
mdl/linterExtensible linting framework with built-in rules and Starlark scripting
mdl/replInteractive REPL interface

3. High-Level API Layer (api/)

The api/ package provides a simplified, fluent builder API inspired by the Mendix Web Extensibility Model API.

classDiagram
    class ModelAPI {
        +writer *Writer
        +reader *Reader
        +currentModule *Module
        +DomainModels *DomainModelsAPI
        +Microflows *MicroflowsAPI
        +Pages *PagesAPI
        +Enumerations *EnumerationsAPI
        +Modules *ModulesAPI
        +New(writer) *ModelAPI
        +SetModule(module) *ModelAPI
    }

    class EntityBuilder {
        +Persistent() *EntityBuilder
        +NonPersistent() *EntityBuilder
        +WithStringAttribute(name, length) *EntityBuilder
        +WithIntegerAttribute(name) *EntityBuilder
        +Build() (*Entity, error)
    }

    class MicroflowBuilder {
        +WithParameter(name, entity) *MicroflowBuilder
        +WithStringParameter(name) *MicroflowBuilder
        +ReturnsBoolean() *MicroflowBuilder
        +Build() (*Microflow, error)
    }

    ModelAPI --> EntityBuilder : creates
    ModelAPI --> MicroflowBuilder : creates
FilePurpose
api/api.goModelAPI entry point with namespace access
api/domainmodels.goEntityBuilder, AssociationBuilder, AttributeBuilder
api/enumerations.goEnumerationBuilder, EnumValueBuilder
api/microflows.goMicroflowBuilder with parameters and return types
api/pages.goPageBuilder, widget builders
api/modules.goModulesAPI for module retrieval

4. SDK Layer (sdk/)

The SDK layer provides Go types and APIs for Mendix model elements.

classDiagram
    class Reader {
        +Open(path) Reader
        +ListModules() []*Module
        +ListDomainModels() []*DomainModel
        +ListMicroflows() []*Microflow
        +ListPages() []*Page
        +FindCustomWidgetType(widgetID) *RawCustomWidgetType
        +Close()
    }

    class Writer {
        +OpenForWriting(path) Writer
        +AddEntity(dm, entity)
        +AddAssociation(dm, assoc)
        +Save()
        +Close()
    }

    class DomainModel {
        +ID
        +ContainerID
        +Entities []*Entity
        +Associations []*Association
    }

    class Entity {
        +ID
        +Name
        +Attributes []*Attribute
        +Source string
        +OqlQuery string
    }

    Reader --> DomainModel
    Writer --> DomainModel
    DomainModel --> Entity
PackagePurpose
sdk/mpr/MPR file format handling (~18k lines across reader, writer, parser files split by domain)
sdk/domainmodelEntity, Attribute, Association types
sdk/microflowsMicroflow, Activity types (60+ types)
sdk/pagesPage, Widget types (50+ types)
sdk/widgetsEmbedded widget templates for pluggable widgets

The sdk/mpr/ package is split by domain for maintainability:

File PatternPurpose
reader.go, reader_*.goRead-only MPR access, split by element type
writer.go, writer_*.goRead-write MPR modification (domainmodel, microflow, security, widgets, etc.)
parser.go, parser_*.goBSON parsing and deserialization (domainmodel, microflow, etc.)
utils.goUUID generation utilities

5. Model Layer (model/)

Core types shared across the SDK:

classDiagram
    class ID {
        <<type alias>>
        string
    }

    class BaseElement {
        +ID ID
        +TypeName string
    }

    class QualifiedName {
        +Module string
        +Name string
    }

    class Module {
        +BaseElement
        +Name string
        +FromAppStore bool
    }

    class Text {
        +Translations map
        +GetTranslation(lang) string
    }

    BaseElement <|-- Module

6. External SQL Layer (sql/)

The sql/ package provides external database connectivity for querying PostgreSQL, Oracle, and SQL Server databases.

flowchart LR
    subgraph "MDL Commands"
        CONNECT["SQL CONNECT"]
        QUERY["SQL alias SELECT ..."]
        IMPORT["IMPORT FROM alias ..."]
        GENERATE["SQL alias GENERATE CONNECTOR"]
    end

    subgraph "sql/ Package"
        CONN[Connection Manager]
        QE[Query Engine]
        IMP[Import Pipeline]
        GEN[Connector Generator]
        META[Schema Introspection]
        FMT[Output Formatters]
    end

    subgraph "Databases"
        PG[(PostgreSQL)]
        ORA[(Oracle)]
        MSSQL[(SQL Server)]
    end

    CONNECT --> CONN
    QUERY --> QE
    IMPORT --> IMP
    GENERATE --> GEN

    CONN --> PG
    CONN --> ORA
    CONN --> MSSQL
    QE --> CONN
    IMP --> CONN
    GEN --> META
    META --> CONN
FilePurpose
driver.goDriverName type, ParseDriver()
connection.goManager, Connection, credential isolation
config.goDSN resolution (env vars, YAML config)
query.goExecute() – query via database/sql
meta.goShowTables(), DescribeTable() via information_schema
import.goIMPORT pipeline: batch insert, ID generation, sequence tracking
generate.goDatabase Connector MDL generation from external schema
typemap.goSQL to Mendix type mapping, DSN to JDBC URL conversion
mendix.goMendix DB DSN builder, table/column name helpers
format.goTable and JSON output formatters

7. VS Code Extension (vscode-mdl/)

The VS Code extension provides MDL language support via an LSP client that communicates with mxcli lsp --stdio.

graph TB
    subgraph "VS Code"
        EXT[extension.ts]
        TREE[Project Tree Provider]
        LINK[Terminal Link Provider]
        CONTENT[MDL Content Provider]
        PREVIEW[Preview Provider]
    end

    subgraph "Preview Renderers"
        DMRENDER[Domain Model]
        MFRENDER[Microflow]
        PGRENDER[Page Wireframe]
        MODRENDER[Module Overview]
        QPRENDER[Query Plan]
    end

    subgraph "mxcli"
        LSP[LSP Server]
    end

    EXT --> TREE
    EXT --> LINK
    EXT --> CONTENT
    EXT --> PREVIEW
    PREVIEW --> DMRENDER
    PREVIEW --> MFRENDER
    PREVIEW --> PGRENDER
    PREVIEW --> MODRENDER
    PREVIEW --> QPRENDER
    EXT -->|stdio| LSP

LSP features include syntax highlighting, parse/semantic diagnostics, completion, symbols, folding, hover, go-to-definition, clickable terminal links, and context menu commands.

8. LSP Server (cmd/mxcli/lsp*.go)

The LSP server is embedded in the mxcli binary:

FilePurpose
lsp.goMain LSP server, hover, go-to-definition
lsp_diagnostics.goParse and semantic error reporting
lsp_completion.goContext-aware completions
lsp_completions_gen.goGenerated completion data
lsp_symbols.goDocument symbols
lsp_folding.goCode folding ranges
lsp_hover.goHover information
lsp_helpers.goShared utilities

Package Structure

Overview of the Go package organization and dependency relationships.

Package Dependency Graph

graph LR
    subgraph "cmd/"
        MXCLI[mxcli/]
        CODEGEN[codegen/]
    end

    subgraph "mdl/"
        GRAMMAR[grammar/]
        ASTPKG[ast/]
        VISITORPKG[visitor/]
        EXECUTORPKG[executor/]
        CATALOGPKG[catalog/]
        LINTERPKG[linter/]
        REPLPKG[repl/]
    end

    subgraph "api/"
        APIPKG[api.go]
        DMAPIMOD[domainmodels.go]
        MFAPIMOD[microflows.go]
        PGAPIMOD[pages.go]
        ENUMAPIMOD[enumerations.go]
        MODAPIMOD[modules.go]
    end

    subgraph "sdk/"
        MPR[mpr/]
        DMPKG[domainmodel/]
        MFPKG[microflows/]
        PGPKG[pages/]
        WIDGETSPKG[widgets/]
    end

    subgraph "sql/"
        SQLCONN[connection.go]
        SQLQUERY[query.go]
        SQLIMPORT[import.go]
        SQLGEN[generate.go]
        SQLMETA[meta.go]
    end

    subgraph "internal/"
        CODEGENINT[codegen/]
    end

    subgraph "vscode-mdl/"
        VSEXT[extension.ts]
        VSLSP[LSP client]
        VSPREVIEW[preview/]
        VSTREE[projectTreeProvider.ts]
    end

    subgraph "Root"
        MODELSDK[modelsdk.go]
        MODELPKG[model/]
    end

    MXCLI --> REPLPKG
    MXCLI --> EXECUTORPKG
    REPLPKG --> EXECUTORPKG
    EXECUTORPKG --> VISITORPKG
    VISITORPKG --> GRAMMAR
    EXECUTORPKG --> MODELSDK
    EXECUTORPKG --> CATALOGPKG
    EXECUTORPKG --> LINTERPKG
    APIPKG --> MPR
    APIPKG --> DMPKG
    APIPKG --> MFPKG
    APIPKG --> PGPKG
    MODELSDK --> MPR
    MODELSDK --> DMPKG
    MODELSDK --> MFPKG
    MODELSDK --> PGPKG
    MPR --> MODELPKG
    VSEXT --> VSLSP

Directory Layout

github.com/mendixlabs/mxcli/
├── modelsdk.go              # Main public API (Open, OpenForWriting, helpers)
├── model/                   # Core types: ID, QualifiedName, Module, Element interface
│
├── api/                     # High-level fluent API
│   ├── api.go               # ModelAPI entry point with namespace access
│   ├── domainmodels.go      # EntityBuilder, AssociationBuilder, AttributeBuilder
│   ├── enumerations.go      # EnumerationBuilder
│   ├── microflows.go        # MicroflowBuilder
│   ├── pages.go             # PageBuilder, widget builders
│   └── modules.go           # ModulesAPI
│
├── sdk/                     # SDK implementation packages
│   ├── domainmodel/         # Entity, Attribute, Association, DomainModel
│   ├── microflows/          # Microflow, Nanoflow, activities (60+ types)
│   ├── pages/               # Page, Layout, Widget types (50+ widgets)
│   ├── widgets/             # Embedded widget templates for pluggable widgets
│   │   ├── loader.go        # Template loading with go:embed
│   │   └── templates/       # JSON widget type definitions by Mendix version
│   └── mpr/                 # MPR file format handling
│       ├── reader.go        # Read-only MPR access
│       ├── writer.go        # Read-write MPR modification
│       ├── parser.go        # BSON parsing and deserialization
│       └── utils.go         # UUID generation utilities
│
├── mdl/                     # MDL parser & CLI
│   ├── grammar/             # ANTLR4 grammar definition
│   │   ├── MDLLexer.g4      # Lexer grammar (tokens)
│   │   ├── MDLParser.g4     # Parser grammar (rules)
│   │   └── parser/          # Generated Go parser code
│   ├── ast/                 # AST node types for MDL statements
│   ├── visitor/             # ANTLR listener to build AST
│   ├── executor/            # Executes AST against modelsdk-go
│   ├── catalog/             # SQLite-based catalog for querying project metadata
│   ├── linter/              # Extensible linting framework
│   │   └── rules/           # Built-in lint rules
│   └── repl/                # Interactive REPL interface
│
├── sql/                     # External database connectivity
│   ├── driver.go            # DriverName type, ParseDriver()
│   ├── connection.go        # Manager, Connection, credential isolation
│   ├── config.go            # DSN resolution (env vars, YAML config)
│   ├── query.go             # Execute() -- query via database/sql
│   ├── meta.go              # ShowTables(), DescribeTable()
│   ├── format.go            # Table and JSON output formatters
│   ├── mendix.go            # Mendix DB DSN builder, table/column name helpers
│   ├── import.go            # IMPORT pipeline: batch insert, ID generation
│   ├── generate.go          # Database Connector MDL generation
│   └── typemap.go           # SQL to Mendix type mapping
│
├── cmd/                     # Command-line tools
│   ├── mxcli/               # CLI entry point (Cobra-based)
│   └── codegen/             # Code generator CLI
│
├── internal/                # Internal packages (not exported)
│   └── codegen/             # Metamodel code generation system
│       ├── schema/          # JSON reflection data loading
│       ├── transform/       # Transform to Go types
│       └── emit/            # Go source code generation
│
├── generated/metamodel/     # Auto-generated type definitions
├── examples/                # Usage examples
│
├── vscode-mdl/              # VS Code extension (TypeScript, uses bun)
│   ├── src/extension.ts     # Extension entry point
│   └── package.json         # Extension manifest
│
└── reference/               # Reference materials (not Go code)
    ├── mendixmodellib/      # TypeScript library + reflection data
    ├── mendixmodelsdk/      # TypeScript SDK reference
    └── mdl-grammar/         # Comprehensive MDL grammar reference

Key Package Responsibilities

PackageExportedDescription
modelsdk (root)YesMain public API: Open(), OpenForWriting(), helper constructors
model/YesCore types: ID, QualifiedName, Module, Element interface
api/YesFluent builder API for domain models, microflows, pages
sdk/domainmodel/YesEntity, Attribute, Association Go types
sdk/microflows/YesMicroflow, Nanoflow, 60+ activity types
sdk/pages/YesPage, Layout, 50+ widget types
sdk/widgets/YesWidget template loading and cloning
sdk/mpr/YesMPR reader, writer, BSON parser
mdl/grammar/YesANTLR4 generated lexer/parser
mdl/ast/YesAST node types for all MDL statements
mdl/visitor/YesParse tree to AST conversion
mdl/executor/YesAST execution against SDK
mdl/catalog/YesSQLite metadata catalog
mdl/linter/YesLinting framework and rules
sql/YesExternal database connectivity
internal/codegen/NoMetamodel code generation (not exported)

Dependencies

DependencyPurpose
modernc.org/sqlitePure Go SQLite driver (no CGO required)
go.mongodb.org/mongo-driverBSON parsing for Mendix document format
github.com/antlr4-go/antlr/v4ANTLR4 Go runtime for MDL parser
github.com/spf13/cobraCLI framework
github.com/jackc/pgx/v5PostgreSQL driver
github.com/sijms/go-ora/v2Oracle driver
github.com/microsoft/go-mssqldbSQL Server driver

Design Decisions

Key architectural decisions made during the development of ModelSDK Go, with rationale and trade-offs.

Pure Go SQLite (No CGO)

Decision: Use modernc.org/sqlite instead of mattn/go-sqlite3.

Rationale: mattn/go-sqlite3 requires CGO and a C compiler, complicating cross-compilation and deployment. The pure Go implementation works on all platforms without external dependencies.

Trade-off: Slightly lower SQLite performance compared to the C implementation. In practice, MPR files are small enough that this is not measurable.

BSON with MongoDB Driver

Decision: Use go.mongodb.org/mongo-driver for BSON parsing.

Rationale: Mendix stores model elements as BSON documents. The MongoDB Go driver provides a mature, well-tested BSON implementation. No actual MongoDB connection is needed – only the BSON codec is used.

SQL-Like DSL (MDL)

Decision: Create a custom SQL-like language instead of using JSON, YAML, or a general-purpose language.

Rationale:

  • SQL is familiar to most developers
  • LLMs generate SQL-like syntax with high accuracy
  • 5-10x more token-efficient than JSON representations
  • Human-readable diffs are easier to review than binary or JSON changes
  • High information density for scanning microflow logic

Trade-off: Requires building and maintaining a custom parser (ANTLR4).

ANTLR4 for Parsing

Decision: Use ANTLR4 instead of hand-written parser or Go-native parser generators.

Rationale:

  • Grammar is shareable across languages (Go, TypeScript, Java, Python)
  • Well-defined grammar file serves as the language specification
  • Rich tooling for grammar debugging and visualization
  • Generated parser handles error recovery automatically

Trade-off: ANTLR4 Go target generates large files. Regeneration requires the ANTLR4 tool (make grammar).

Storage Names in BSON

Decision: Always use storageName (not qualifiedName) for the $Type field in BSON.

Rationale: Studio Pro uses storage names internally. Using qualified names causes TypeCacheUnknownTypeException. The two names are often identical, but critical exceptions exist (e.g., DomainModels$Entity vs DomainModels$EntityImpl).

Inverted Association Pointer Names

Decision: Preserve Mendix’s counter-intuitive naming where ParentPointer points to the FROM entity and ChildPointer points to the TO entity.

Rationale: Changing the field names would create a mapping layer that hides the actual BSON structure, making debugging harder. By keeping the original names, developers can directly compare Go structs with raw BSON output.

Trade-off: Every developer working with associations must learn the inverted semantics. Documentation and comments are essential.

Embedded Widget Templates

Decision: Embed widget templates as JSON files compiled into the binary via go:embed.

Rationale: Pluggable widgets require exact property schemas that change between Mendix versions. Embedding templates ensures the binary is self-contained and version-matched. Extracting templates from Studio Pro guarantees correctness.

Trade-off: New Mendix versions or widgets require extracting and adding new template files.

Catalog as SQLite

Decision: Use an in-memory SQLite database for the project metadata catalog.

Rationale: SQLite provides indexing, FTS5 full-text search, and standard SQL querying without any external dependency. The catalog is rebuilt from the MPR on each session, so persistence is not needed.

Fluent API Layer

Decision: Provide both a low-level SDK (direct struct construction) and a high-level fluent API (api/ package).

Rationale: The low-level SDK gives full control over BSON structure, which is needed for complex operations. The fluent API reduces boilerplate for common tasks and makes test code more readable.

Trade-off: Two API surfaces to maintain and document.

MPR File Format

Mendix projects are stored in .mpr files, which are SQLite databases containing BSON-encoded model elements. This page provides an overview of the format; see v1 vs v2 for version-specific details.

Structure

An MPR file is a standard SQLite database with two key tables:

Unit Table

The Unit table stores document metadata:

ColumnDescription
UnitIDBinary UUID identifying the document
ContainerIDParent module UUID
ContainmentNameRelationship name (e.g., documents)
UnitTypeFully qualified type name
NameDocument name

UnitContents Table (v1 only)

In MPR v1, the UnitContents table stores the actual BSON document content:

ColumnDescription
UnitIDBinary UUID matching the Unit table
ContentsBSON blob containing the full document

In MPR v2, document contents are stored as individual files in the mprcontents/ folder instead.

Unit Types

Every document in a Mendix project has a unit type:

UnitTypeDocument Type
DomainModels$DomainModelDomain model (entities, associations)
DomainModels$ViewEntitySourceDocumentOQL query for VIEW entities
Microflows$MicroflowMicroflow definition
Microflows$NanoflowNanoflow definition
Pages$PagePage definition
Pages$LayoutLayout definition
Pages$SnippetSnippet definition
Pages$BuildingBlockBuilding block definition
Enumerations$EnumerationEnumeration definition
JavaActions$JavaActionJava action definition
Security$ProjectSecurityProject security settings
Security$ModuleSecurityModule security settings
Navigation$NavigationDocumentNavigation profile
Settings$ProjectSettingsProject settings
BusinessEvents$BusinessEventServiceBusiness event service

BSON Document Structure

Every BSON document contains at minimum:

FieldTypeDescription
$IDBinary (UUID)Unique identifier for this element
$TypeStringFully qualified type name (using storageName, not qualifiedName)

ID Format

IDs are stored as BSON Binary subtype 0 (generic) containing UUID bytes:

{
  "$ID": {
    "Subtype": 0,
    "Data": "base64-encoded-uuid"
  }
}

Array Convention

Arrays in Mendix BSON have an integer count as the first element:

{
  "Attributes": [
    3,              // Count/type prefix
    { ... },        // First attribute
    { ... }         // Second attribute
  ]
}

The prefix value (typically 2 or 3) indicates the array type. When writing arrays, you must include this prefix. When parsing, skip the first element.

Reference Types

Reference TypeStorage FormatExample
BY_ID_REFERENCEBinary UUIDIndex AttributePointer
BY_NAME_REFERENCEQualified name stringValidationRule Attribute
PARTEmbedded BSON objectChild objects serialized inline

Using the wrong reference format causes Studio Pro to fail loading the model. Always check the metamodel reflection data to determine which format each property uses.

Storage Names vs Qualified Names

The $Type field in BSON must use the storageName, not the qualifiedName. These are often identical but not always:

qualifiedName (SDK)storageName (BSON $Type)
DomainModels$EntityDomainModels$EntityImpl
DomainModels$IndexDomainModels$EntityIndex

Using the wrong name causes TypeCacheUnknownTypeException when opening in Studio Pro. See Storage Names for more details.

v1 (SQLite) vs v2 (mprcontents/)

Mendix uses two MPR file format versions. The library auto-detects and handles both.

MPR v1 (Mendix < 10.18)

A single .mpr SQLite database file containing all model data.

Storage

  • Unit table: document metadata (name, type, container)
  • UnitContents table: BSON document blobs

Characteristics

  • Self-contained single file
  • Larger file size (entire project in one database)
  • Binary diffs make Git versioning difficult
  • All documents read/written through SQLite queries

Reading

// Auto-detected by modelsdk.Open()
reader, _ := modelsdk.Open("/path/to/project.mpr")
// reader.Version() returns 1

MPR v2 (Mendix >= 10.18)

An .mpr metadata file plus a mprcontents/ folder with individual document files.

Storage

  • .mpr file: SQLite with Unit table (metadata only, no UnitContents)
  • mprcontents/ folder: individual .mxunit files containing BSON

Each .mxunit file is named by the document’s UUID and contains the raw BSON for that document.

Directory Layout

project/
├── app.mpr                    # SQLite metadata
└── mprcontents/
    ├── 2a3b4c5d-...           # Domain model BSON
    ├── 6e7f8a9b-...           # Microflow BSON
    ├── c0d1e2f3-...           # Page BSON
    └── ...

Characteristics

  • Better for Git versioning – individual files change independently
  • Smaller diffs when modifying a single document
  • Parallel read access to different documents
  • Requires both the .mpr file and the mprcontents/ folder

Reading

// Auto-detected by modelsdk.Open()
reader, _ := modelsdk.Open("/path/to/project.mpr")
// reader.Version() returns 2

Format Detection

The library detects the format by checking whether the UnitContents table exists in the SQLite database:

ConditionFormat
UnitContents table exists and has rowsv1
UnitContents table missing or empty, mprcontents/ folder existsv2

This detection is automatic – callers of Open() and OpenForWriting() do not need to specify the format.

Comparison

Featurev1v2
Mendix version< 10.18>= 10.18
File structureSingle .mpr.mpr + mprcontents/
Document storageSQLite UnitContents tableIndividual .mxunit files
Git friendlinessPoor (binary diffs)Good (per-document files)
File sizeLarger single fileDistributed across files
Read performanceSingle DB queryFile I/O per document
Write granularityFull DB transactionPer-file write

Writing Behavior

When writing with OpenForWriting():

  • v1: Documents are written as BSON blobs into the UnitContents table within a SQLite transaction
  • v2: Documents are written as individual .mxunit files in the mprcontents/ folder; the Unit table metadata is updated in SQLite

The writer handles both formats transparently.

BSON Document Structure

How Mendix project documents are serialized in BSON format, including the role of $Type fields, nested structures, array version markers, and widget serialization.

Source of Truth

The authoritative reference for BSON serialization is the reflection-data at:

reference/mendixmodellib/reflection-data/{version}-structures.json

Each structure entry contains:

  • qualifiedName: The API name (e.g., Pages$DivContainer)
  • storageName: The BSON $Type value (e.g., Forms$DivContainer)
  • defaultSettings: Required default property values
  • properties: Property definitions with types and requirements

Type Name Mapping

Mendix uses different prefixes for API names vs storage names:

API PrefixStorage PrefixDomain
Pages$Forms$Page widgets
Microflows$Microflows$Microflow elements
DomainModels$DomainModels$Domain model elements
Texts$Texts$Text/translation elements
DataTypes$DataTypes$Data type definitions
CustomWidgets$CustomWidgets$Pluggable widgets

Common Type Name Mistakes

Incorrect (will fail)Correct Storage Name
Forms$NoClientActionForms$NoAction
Forms$PageClientActionForms$FormAction
Forms$MicroflowClientActionForms$MicroflowAction
Pages$DivContainerForms$DivContainer
Pages$ActionButtonForms$ActionButton

Unit Document Structure

Each model element (entity, microflow, page, etc.) is stored as a BSON document:

graph LR
    subgraph "BSON Document"
        ID["$ID: binary UUID"]
        TYPE["$Type: 'DomainModels$Entity'"]
        NAME["Name: 'Customer'"]
        ATTRS["Attributes: [...]"]
        LOC["Location: '100;200'"]
    end

Array Version Markers

Mendix BSON uses version markers for arrays:

MarkerMeaning
[3]Empty array
[2, item1, item2, ...]Non-empty array with items
[3, item1, item2, ...]Non-empty array (text items)

Example:

"Widgets": [3]           // Empty widgets array
"Widgets": [2, {...}]    // One widget
"Items": [3, {...}]      // One text translation item

Important: These version markers only exist in BSON format, not in JSON templates. In JSON, [2] is an array containing the integer 2, not an empty array with a version marker.

Page Structure

A page document has this top-level structure:

{
  "$ID": "<uuid>",
  "$Type": "Forms$Page",
  "AllowedModuleRoles": [1],
  "Appearance": { ... },
  "Autofocus": "DesktopOnly",
  "CanvasHeight": 600,
  "CanvasWidth": 1200,
  "Documentation": "",
  "Excluded": false,
  "ExportLevel": "Hidden",
  "FormCall": { ... },
  "Name": "PageName",
  "Parameters": [3, ...],
  "PopupCloseAction": "",
  "Title": { ... },
  "Url": "page_url",
  "Variables": [3]
}

Appearance Object

Standard appearance object for widgets:

{
  "$ID": "<uuid>",
  "$Type": "Forms$Appearance",
  "Class": "",
  "DesignProperties": [3],
  "DynamicClasses": "",
  "Style": ""
}

Widget Default Properties

Each widget type requires specific default properties to be serialized. Studio Pro will fail to load the project if required properties are missing.

DivContainer (Container)

{
  "$Type": "Forms$DivContainer",
  "Appearance": { ... },
  "ConditionalVisibilitySettings": null,
  "Name": "",
  "NativeAccessibilitySettings": null,
  "OnClickAction": { "$Type": "Forms$NoAction", ... },
  "RenderMode": "Div",
  "ScreenReaderHidden": false,
  "TabIndex": 0,
  "Widgets": [3]
}

DataView

{
  "$Type": "Forms$DataView",
  "Appearance": { ... },
  "ConditionalEditabilitySettings": null,
  "ConditionalVisibilitySettings": null,
  "DataSource": { ... },
  "Editability": "Always",
  "FooterWidgets": [3],
  "LabelWidth": 3,
  "Name": "",
  "NoEntityMessage": { ... },
  "ReadOnlyStyle": "Control",
  "ShowFooter": true,
  "TabIndex": 0,
  "Widgets": [3]
}

Input Widgets (TextBox, TextArea, DatePicker, etc.)

Input widgets require several non-null properties:

{
  "$Type": "Forms$TextBox",
  "Appearance": { ... },
  "AriaRequired": false,
  "AttributeRef": {
    "$ID": "<uuid>",
    "$Type": "DomainModels$AttributeRef",
    "Attribute": "Module.Entity.AttributeName",
    "EntityRef": null
  },
  "ConditionalEditabilitySettings": null,
  "ConditionalVisibilitySettings": null,
  "Editable": "Always",
  "FormattingInfo": { ... },
  "IsPasswordBox": false,
  "LabelTemplate": { ... },
  "Name": "textBox1",
  "NativeAccessibilitySettings": null,
  "OnChangeAction": { "$Type": "Forms$NoAction", ... },
  "OnEnterAction": { "$Type": "Forms$NoAction", ... },
  "OnEnterKeyPressAction": { "$Type": "Forms$NoAction", ... },
  "OnLeaveAction": { "$Type": "Forms$NoAction", ... },
  "PlaceholderTemplate": { ... },
  "ReadOnlyStyle": "Inherit",
  "TabIndex": 0,
  "Validation": { ... }
}

Required nested objects:

  • AttributeRef – Must have Attribute as fully qualified path (e.g., Module.Entity.AttributeName)
  • FormattingInfo – Required for TextBox and DatePicker
  • PlaceholderTemplate – Required Forms$ClientTemplate object
  • Validation – Required Forms$WidgetValidation object

Attribute Path Resolution

The AttributeRef.Attribute field requires a fully qualified path in the format Module.Entity.AttributeName. When using short attribute names in MDL, the SDK automatically resolves them using the DataView’s entity context.

Client Action Types

Action TypeStorage Name
No ActionForms$NoAction
Save ChangesForms$SaveChangesClientAction
Cancel ChangesForms$CancelChangesClientAction
Close PageForms$ClosePageClientAction
DeleteForms$DeleteClientAction
Show PageForms$FormAction
Call MicroflowForms$MicroflowAction
Call NanoflowForms$CallNanoflowClientAction

Pluggable Widgets (CustomWidget)

Pluggable widgets like ComboBox use CustomWidgets$CustomWidget with a complex nested structure:

{
  "$Type": "CustomWidgets$CustomWidget",
  "Appearance": { ... },
  "Name": "comboBox1",
  "Object": {
    "$Type": "CustomWidgets$WidgetObject",
    "Properties": [2, { ... }],
    "TypePointer": "<binary ID referencing ObjectType>"
  },
  "Type": {
    "$Type": "CustomWidgets$CustomWidgetType",
    "ObjectType": {
      "$Type": "CustomWidgets$WidgetObjectType",
      "PropertyTypes": [2, { ... }]
    },
    "WidgetId": "com.mendix.widget.web.combobox.Combobox"
  }
}

TypePointer References

There are three levels of TypePointer references:

  1. WidgetObject.TypePointer references ObjectType.$ID (the WidgetObjectType)
  2. WidgetProperty.TypePointer references PropertyType.$ID (the WidgetPropertyType)
  3. WidgetValue.TypePointer references ValueType.$ID (the WidgetValueType)
CustomWidgetType
└── ObjectType (WidgetObjectType)           ← WidgetObject.TypePointer
    └── PropertyTypes[]
        └── PropertyType (WidgetPropertyType) ← WidgetProperty.TypePointer
            └── ValueType (WidgetValueType)   ← WidgetValue.TypePointer

If any TypePointer is missing or references an invalid ID, Studio Pro will fail to load the widget.

Widget Size

Each pluggable widget instance contains a full copy of both Type and Object:

ComponentBSON SizeDescription
Type (CustomWidgetType)~54 KBWidget definition with all PropertyTypes
Object (WidgetObject)~34 KBProperty values for all PropertyTypes
Total per widget~88 KB

There is no deduplication within a page. A page with 4 ComboBox widgets requires ~352 KB for widgets alone.

Common Errors

ErrorCauseSolution
“The type cache does not contain a type with qualified name X”Incorrect $Type valueCheck reflection-data for correct storage name
“No entity configured for the data source”Missing or incorrect DataView DataSourceConfigure Forms$DataViewSource with proper EntityRef
CE0463 “widget definition has changed”Object properties don’t match Type PropertyTypesUse template Object as base, only modify needed properties
“Project uses features that are no longer supported”Missing widget default propertiesInclude all required defaults from reflection-data

Files Reference

FilePurpose
sdk/mpr/writer_widgets.goWidget serialization to BSON
sdk/mpr/writer_pages.goPage serialization
sdk/mpr/reader_widgets.goWidget template extraction and cloning
sdk/mpr/parser_page.goPage deserialization
sdk/widgets/loader.goEmbedded template loading
sdk/widgets/templates/mendix-11.6/*.jsonEmbedded widget templates
reference/mendixmodellib/reflection-data/*.jsonType definitions

Storage Names vs Qualified Names

Mendix uses different “storage names” in BSON $Type fields than the “qualified names” shown in the TypeScript SDK documentation. Using the wrong name causes a TypeCacheUnknownTypeException when opening the project in Studio Pro.

The Problem

When serializing model elements to BSON, the $Type field must use the storage name, not the qualified name from the SDK documentation. These names differ in ways that are not always predictable.

Domain Prefix Mapping

The most systematic difference is the domain prefix used for page-related types:

API PrefixStorage PrefixDomain
Pages$Forms$Page widgets
Microflows$Microflows$Microflow elements (same)
DomainModels$DomainModels$Domain model elements (same)
Texts$Texts$Text/translation elements (same)
DataTypes$DataTypes$Data type definitions (same)
CustomWidgets$CustomWidgets$Pluggable widgets (same)

The Pages$ to Forms$ renaming reflects Mendix’s historical terminology – “Form” was the original term for “Page”.

Microflow Action Name Mapping

Several microflow actions have storage names that differ significantly from their qualified names:

Qualified Name (SDK/docs)Storage Name (BSON $Type)Note
CreateObjectActionCreateChangeAction
ChangeObjectActionChangeAction
DeleteObjectActionDeleteAction
CommitObjectsActionCommitAction
RollbackObjectActionRollbackAction
AggregateListActionAggregateAction
ListOperationActionListOperationsActionNote the plural
ShowPageActionShowFormAction“Form” was original for “Page”
ClosePageActionCloseFormAction“Form” was original for “Page”

Client Action Name Mapping

Page client actions also have storage name differences:

Incorrect (will fail)Correct Storage Name
Forms$NoClientActionForms$NoAction
Forms$PageClientActionForms$FormAction
Forms$MicroflowClientActionForms$MicroflowAction
Pages$DivContainerForms$DivContainer
Pages$ActionButtonForms$ActionButton

How to Verify Storage Names

When adding new types, always verify the storage name by:

  1. Examining existing MPR files with the mx tool or an SQLite browser
  2. Checking the reflection data in reference/mendixmodellib/reflection-data/
  3. Looking at the parser cases in sdk/mpr/parser_microflow.go

Querying Reflection Data

Use this Python snippet to check widget default settings:

import json

with open('reference/mendixmodellib/reflection-data/11.0.0-structures.json') as f:
    data = json.load(f)

# Find widget by API name
widget = data.get('Pages$DivContainer', {})
print('Storage name:', widget.get('storageName'))
print('Defaults:', json.dumps(widget.get('defaultSettings', {}), indent=2))

# Search by storage name
for key, val in data.items():
    if val.get('storageName') == 'Forms$NoAction':
        print(f'{key}: {val.get("defaultSettings")}')

Association Parent/Child Pointer Semantics

Mendix BSON uses inverted naming for association pointers, which is counter-intuitive:

BSON FieldPoints ToMDL Keyword
ParentPointerFROM entity (FK owner)FROM Module.Child
ChildPointerTO entity (referenced)TO Module.Parent

For example, CREATE ASSOCIATION Mod.Child_Parent FROM Mod.Child TO Mod.Parent stores:

  • ParentPointer = Child.$ID (the FROM entity owns the foreign key)
  • ChildPointer = Parent.$ID (the TO entity is being referenced)

This affects entity access rules: MemberAccess entries for associations must only be added to the FROM entity (the one stored in ParentPointer). Adding them to the TO entity triggers CE0066 “Entity access is out of date”.

Key Takeaway

When unsure about the correct BSON structure for a new feature, create a working example in Mendix Studio Pro and compare the generated BSON against a known-good reference. The reflection data at reference/mendixmodellib/reflection-data/ is the definitive source for storage names and default values.

Widget Template System

Pluggable widgets (DataGrid2, ComboBox, Gallery, etc.) require embedded template definitions for correct BSON serialization. This page explains how the template system works.

Why Templates Are Needed

Pluggable widgets in Mendix are defined by two components:

  1. Type (PropertyTypes schema) – defines what properties the widget accepts
  2. Object (WidgetObject defaults) – provides default values for all properties

Both must be present in the BSON output. The type defines the schema; the object provides a valid instance. If the object’s property structure does not match the type’s schema, Studio Pro reports CE0463 “widget definition changed”.

Template Location

Templates are embedded in the binary via Go’s go:embed directive:

sdk/widgets/
├── loader.go                  # Template loading with go:embed
└── templates/
    ├── README.md              # Template extraction requirements
    ├── 10.6.0/               # Templates by Mendix version
    │   ├── DataGrid2.json
    │   ├── ComboBox.json
    │   ├── Gallery.json
    │   └── ...
    └── 11.6.0/
        ├── DataGrid2.json
        └── ...

Each JSON file contains both the type and object fields for a specific widget at a specific Mendix version.

Template Structure

A template JSON file looks like:

{
  "type": {
    "$Type": "CustomWidgets$WidgetPropertyTypes",
    "Properties": [
      { "Name": "columns", "Type": "Object", ... },
      { "Name": "showLabel", "Type": "Boolean", ... }
    ]
  },
  "object": {
    "$Type": "CustomWidgets$WidgetObject",
    "Properties": [
      { "Name": "columns", "Value": [] },
      { "Name": "showLabel", "Value": true }
    ]
  }
}

Extracting Templates

Templates must be extracted from Studio Pro-created widgets, not generated programmatically. The extraction process:

  1. Create a widget instance in Studio Pro
  2. Save the project
  3. Extract the BSON from the .mpr file
  4. Convert the type and object sections to JSON
  5. Save as a template file

This ensures the property structure exactly matches what Studio Pro expects.

Important: Programmatically generated templates often have subtle differences in property ordering, default values, or nested structures that cause CE0463 errors. Always extract from Studio Pro.

Loading Templates at Runtime

The loader.go file provides functions to load templates:

// Load a widget template for the project's Mendix version
template, err := widgets.LoadTemplate("DataGrid2", mendixVersion)

The loader searches for the most specific version match, falling back to earlier versions if an exact match is not available.

JSON Templates vs BSON Serialization

When editing template JSON files, use standard JSON conventions:

  • Empty arrays are [] (not [3])
  • Booleans are true/false
  • Strings are "quoted"

The array count prefixes ([3, ...]) required by Mendix BSON are added automatically during serialization. Writing [2] in a JSON template creates an array containing the integer 2, not an empty BSON array.

Debugging CE0463 Errors

If a page fails with CE0463 after creation:

  1. Create the same widget manually in Studio Pro
  2. Extract its BSON from the saved project
  3. Compare the template’s object properties against the Studio Pro version
  4. Look for missing properties, wrong default values, or incorrect nesting
  5. Update the template JSON to match

See the debug workflow in .claude/skills/debug-bson.md for detailed steps.

MDL Parser

The MDL parser translates SQL-like MDL (Mendix Definition Language) syntax into executable operations against Mendix project files. It uses ANTLR4 for grammar definition, enabling cross-language grammar sharing with other implementations (TypeScript, Java, Python).

Architecture Overview

┌─────────────────────────────────────────────────────────────────────┐
│                         MDL Input String                            │
│              "SHOW ENTITIES IN MyModule"                            │
└─────────────────────────────────────────────────────────────────────┘
                                  │
                                  ▼
┌─────────────────────────────────────────────────────────────────────┐
│                    ANTLR4 Lexer (mdl_lexer.go)                      │
│    Generated from MDLLexer.g4 - Tokenizes input into SHOW, ENTITIES,│
│    IN, IDENTIFIER tokens                                            │
└─────────────────────────────────────────────────────────────────────┘
                                  │
                                  ▼
┌─────────────────────────────────────────────────────────────────────┐
│                    ANTLR4 Parser (mdl_parser.go)                    │
│    Generated from MDLParser.g4 - Builds parse tree according to     │
│    grammar rules                                                    │
└─────────────────────────────────────────────────────────────────────┘
                                  │
                                  ▼
┌─────────────────────────────────────────────────────────────────────┐
│                    ANTLR Listener (visitor/visitor.go)              │
│    Walks parse tree and builds strongly-typed AST nodes            │
└─────────────────────────────────────────────────────────────────────┘
                                  │
                                  ▼
┌─────────────────────────────────────────────────────────────────────┐
│                         AST (ast/ast.go)                            │
│    *ast.ShowStmt{Type: "ENTITIES", Module: "MyModule"}             │
└─────────────────────────────────────────────────────────────────────┘
                                  │
                                  ▼
┌─────────────────────────────────────────────────────────────────────┐
│                    Executor (executor/executor.go)                  │
│    Executes AST against modelsdk-go API                            │
└─────────────────────────────────────────────────────────────────────┘
                                  │
                                  ▼
┌─────────────────────────────────────────────────────────────────────┐
│                      modelsdk-go Library                            │
│    mpr.Writer, domainmodel.Entity, etc.                            │
└─────────────────────────────────────────────────────────────────────┘

Directory Structure

mdl/
├── grammar/
│   ├── MDLLexer.g4         # ANTLR4 lexer grammar (tokens)
│   ├── MDLParser.g4        # ANTLR4 parser grammar (rules)
│   └── parser/             # Generated parser code (DO NOT EDIT)
│       ├── mdl_lexer.go
│       ├── mdl_parser.go
│       ├── mdlparser_listener.go
│       └── mdlparser_base_listener.go
├── ast/
│   └── ast.go, ast_microflow.go, ast_expression.go, ast_datatype.go, ...
├── visitor/
│   └── visitor.go          # ANTLR listener implementation
├── executor/
│   ├── executor.go              # AST execution logic
│   ├── cmd_microflows_builder.go  # Microflow builder (variable tracking)
│   └── validate_microflow.go     # AST-level semantic checks (mxcli check)
├── catalog/
│   └── catalog.go          # SQLite-based project metadata catalog
├── linter/
│   ├── linter.go           # Linting framework
│   └── rules/              # Built-in lint rules
└── repl/
    └── repl.go             # Interactive REPL interface

Why ANTLR4?

ConsiderationANTLR4Parser Combinators
Cross-languageSame grammar for Go, TS, JavaRewrite per language
Grammar docsEBNF-like, readableCode is the doc
Error messagesBuilt-in recoveryCustom implementation
PerformanceOptimized lexer/parserComparable
ToolingANTLR Lab, IDE pluginsLimited

Why Listener Pattern (not Visitor)?

MDL uses the listener pattern rather than the visitor pattern:

  • Listener: Callbacks fired during tree walk, simpler for AST building
  • Visitor: Returns values from each node, better for expression evaluation

For MDL, statements are independent and do not need return value propagation, making the listener pattern more appropriate.

Regenerating the Parser

After modifying MDLLexer.g4 or MDLParser.g4:

make grammar

Or manually:

cd mdl/grammar
antlr4 -Dlanguage=Go -package parser -o parser MDLLexer.g4 MDLParser.g4

Requirements:

  • ANTLR4 tool (antlr4 command or Java JAR)
  • Go target runtime (github.com/antlr4-go/antlr/v4)

ANTLR4 Grammar Design

The MDL parser uses ANTLR4 with separate lexer and parser grammars. This page covers the design patterns and key decisions in the grammar.

Grammar Files

FilePurpose
mdl/grammar/MDLLexer.g4Token definitions (keywords, operators, literals)
mdl/grammar/MDLParser.g4Parser rules (statements, expressions, blocks)
mdl/grammar/parser/Generated Go code (do not edit)

Regenerating the Parser

After modifying either .g4 file:

make grammar

This runs the ANTLR4 tool to regenerate the Go parser code in mdl/grammar/parser/.

Key Design Patterns

Case-Insensitive Keywords

MDL keywords are case-insensitive. This is implemented using fragment rules in the lexer:

// Lexer fragments for case-insensitive matching
fragment A : [aA] ;
fragment B : [bB] ;
// ...

// Keywords use fragments
SHOW    : S H O W ;
CREATE  : C R E A T E ;
ENTITY  : E N T I T Y ;

This means SHOW, show, Show, and sHoW are all valid.

Separate Lexer and Parser

The grammar uses split lexer/parser grammars (not a combined grammar). This provides:

  • Clearer separation between tokenization and parsing
  • Better control over token modes
  • Easier maintenance of complex grammars

Statement Structure

Top-level statements follow a consistent pattern:

statement
    : showStatement
    | describeStatement
    | createStatement
    | alterStatement
    | dropStatement
    | grantStatement
    | revokeStatement
    | searchStatement
    | refreshStatement
    | sqlStatement
    | importStatement
    // ...
    ;

Each statement type has its own rule, keeping the grammar modular.

Qualified Names

Module-qualified names (Module.Entity) are a fundamental pattern:

qualifiedName
    : IDENTIFIER ('.' IDENTIFIER)*
    ;

This matches Customer, Sales.Customer, and Sales.Customer.Name.

Widget Trees

Page definitions use recursive widget nesting with curly braces:

widgetDecl
    : widgetType IDENTIFIER widgetProperties? widgetBody?
    ;

widgetBody
    : '{' widgetDecl* '}'
    ;

This allows arbitrary nesting depth for layout grids, containers, and data views.

Architecture Pipeline

MDL Input String
       │
       ▼
┌──────────────┐
│ ANTLR4 Lexer │  Tokenizes into SHOW, IDENTIFIER, etc.
└──────┬───────┘
       │
       ▼
┌──────────────┐
│ ANTLR4 Parser│  Builds parse tree from grammar rules
└──────┬───────┘
       │
       ▼
┌──────────────┐
│   Visitor    │  Walks parse tree, builds typed AST nodes
│  (Listener)  │  (mdl/visitor/visitor.go)
└──────┬───────┘
       │
       ▼
┌──────────────┐
│     AST      │  Strongly-typed statement nodes
│              │  (mdl/ast/ast.go)
└──────┬───────┘
       │
       ▼
┌──────────────┐
│   Executor   │  Executes AST against modelsdk-go
│              │  (mdl/executor/executor.go)
└──────────────┘

Visitor (Listener) Pattern

ANTLR4 generates a listener interface. The MDL visitor (mdl/visitor/visitor.go) implements this interface to build AST nodes:

  • EnterCreateEntityStatement -> creates *ast.CreateEntityStmt
  • EnterCreateMicroflowStatement -> creates *ast.CreateMicroflowStmt
  • EnterShowStatement -> creates *ast.ShowStmt

Each listener method extracts data from the parse tree context and constructs the corresponding AST node.

Error Recovery

The ANTLR4 parser provides built-in error recovery. When a syntax error is encountered:

  1. The parser reports the error with line/column position
  2. It attempts to recover by consuming or inserting tokens
  3. Parsing continues to find additional errors

This enables the language server to report multiple syntax errors in a single pass.

Adding New Syntax

See Adding New Statements for the step-by-step process of extending the grammar.

Lexer - Parser - AST - Executor Pipeline

The complete parsing pipeline from raw MDL text through lexical analysis, parsing, AST construction, and execution against the model.

Pipeline Flow

flowchart TD
    INPUT[/"SHOW ENTITIES IN MyModule"/]

    subgraph Parsing
        LEXER[ANTLR4 Lexer]
        PARSER[ANTLR4 Parser]
        TREE[Parse Tree]
    end

    subgraph "AST Building"
        VISITOR[AST Visitor]
        AST[ShowStmt AST Node]
    end

    subgraph Execution
        EXEC[Executor]
        SDK[SDK API Call]
        FORMAT[Format Output]
    end

    OUTPUT[/Markdown Table/]

    INPUT --> LEXER
    LEXER --> PARSER
    PARSER --> TREE
    TREE --> VISITOR
    VISITOR --> AST
    AST --> EXEC
    EXEC --> SDK
    SDK --> FORMAT
    FORMAT --> OUTPUT

Stage 1: ANTLR4 Grammar

The grammar defines MDL syntax using ANTLR4’s EBNF-like notation.

Case-Insensitive Keywords

Uses fragment rules for case-insensitive matching:

// Keywords are case-insensitive
SHOW    : S H O W ;
ENTITY  : E N T I T Y ;

// Fragment rules for each letter
fragment S : [sS] ;
fragment H : [hH] ;
fragment O : [oO] ;
fragment W : [wW] ;
// ... etc

This allows SHOW, show, Show, etc. to all match the same token.

Labeled Alternatives

Parser rules use labeled alternatives for type-safe listener methods:

showStatement
    : SHOW MODULES SEMI?                       # ShowModules
    | SHOW ENTITIES (IN IDENTIFIER)? SEMI?     # ShowEntities
    | SHOW ENTITY qualifiedName SEMI?          # ShowEntity
    ;

Each label generates a specific listener method (e.g., EnterShowModules, EnterShowEntities).

Whitespace Handling

Whitespace is sent to a hidden channel (skipped):

WS : [ \t\r\n]+ -> skip ;

Stage 2: Generated Parser

ANTLR4 generates four files from the grammar:

FilePurpose
mdl_lexer.goTokenizer – converts input to token stream
mdl_parser.goParser – builds parse tree from tokens
mdl_listener.goListener interface – callbacks for each rule
mdl_base_listener.goEmpty listener implementation for extension

Stage 3: AST Types

Strongly-typed AST nodes representing MDL statements:

// Statement is the interface for all MDL statements
type Statement interface {
    statementNode()
}

// ShowStmt represents SHOW commands
type ShowStmt struct {
    Type   string        // MODULES, ENTITIES, ASSOCIATIONS, ENUMERATIONS
    Module string        // Optional: filter by module
    Name   QualifiedName // For SHOW ENTITY/ASSOCIATION
}

// CreateEntityStmt represents CREATE ENTITY
type CreateEntityStmt struct {
    Name        QualifiedName
    Persistent  bool
    Attributes  []Attribute
    Position    *Position
    Comment     string
    Doc         string
}

// QualifiedName represents Module.Name or just Name
type QualifiedName struct {
    Module string
    Name   string
}

Stage 4: ANTLR Listener (Visitor)

The visitor walks the ANTLR parse tree and builds AST nodes.

Type Assertions for Context Access

func (v *Visitor) EnterShowEntities(ctx *parser.ShowEntitiesContext) {
    stmt := &ast.ShowStmt{Type: "ENTITIES"}

    // Access IDENTIFIER token if present (IN clause)
    if id := ctx.IDENTIFIER(); id != nil {
        stmt.Module = id.GetText()
    }

    v.program.Statements = append(v.program.Statements, stmt)
}

Building Qualified Names

func buildQualifiedName(ctx parser.IQualifiedNameContext) ast.QualifiedName {
    qn := ctx.(*parser.QualifiedNameContext)
    ids := qn.AllIDENTIFIER()

    if len(ids) == 1 {
        return ast.QualifiedName{Name: ids[0].GetText()}
    }
    return ast.QualifiedName{
        Module: ids[0].GetText(),
        Name:   ids[1].GetText(),
    }
}

Error Handling

Syntax errors are collected via a custom error listener:

type ErrorListener struct {
    *antlr.DefaultErrorListener
    Errors []error
}

func (e *ErrorListener) SyntaxError(recognizer antlr.Recognizer, offendingSymbol interface{},
    line, column int, msg string, ex antlr.RecognitionException) {
    e.Errors = append(e.Errors, fmt.Errorf("line %d:%d %s", line, column, msg))
}

Stage 5: Executor

Executes AST statements against the modelsdk-go API:

type Executor struct {
    writer *mpr.Writer
    output io.Writer
}

func (e *Executor) Execute(stmt ast.Statement) error {
    switch s := stmt.(type) {
    case *ast.ConnectStmt:
        return e.executeConnect(s)
    case *ast.ShowStmt:
        return e.executeShow(s)
    case *ast.CreateEntityStmt:
        return e.executeCreateEntity(s)
    // ... other statement types
    }
}

Integration with SDK

func (e *Executor) executeCreateEntity(stmt *ast.CreateEntityStmt) error {
    // Build domain model entity
    entity := &domainmodel.Entity{
        ID:   mpr.GenerateID(),
        Name: stmt.Name.Name,
        // ... other fields
    }

    // Get module and add entity
    module := e.getOrCreateModule(stmt.Name.Module)
    dm := module.DomainModel
    dm.Entities = append(dm.Entities, entity)

    return nil
}

Stage 6: REPL

Interactive read-eval-print loop ties the pipeline together:

type REPL struct {
    executor *executor.Executor
    input    io.Reader
    output   io.Writer
}

func (r *REPL) Run() error {
    scanner := bufio.NewScanner(r.input)
    for {
        fmt.Fprint(r.output, "mdl> ")
        if !scanner.Scan() {
            break
        }

        input := scanner.Text()
        prog, errs := visitor.Build(input)
        if len(errs) > 0 {
            // Handle parse errors
            continue
        }

        for _, stmt := range prog.Statements {
            if err := r.executor.Execute(stmt); err != nil {
                fmt.Fprintf(r.output, "Error: %v\n", err)
            }
        }
    }
    return nil
}

Extending the Parser

Adding a New Statement Type

  1. Update grammar (MDLLexer.g4 for tokens, MDLParser.g4 for rules):
ddlStatement
    : createStatement
    | newStatement      // Add new statement
    ;

newStatement
    : NEW KEYWORD qualifiedName SEMI?    # NewKeyword
    ;

NEW : N E W ;
KEYWORD : K E Y W O R D ;
  1. Regenerate parser:
make grammar
  1. Add AST type (ast/ast.go):
type NewKeywordStmt struct {
    Name QualifiedName
}

func (*NewKeywordStmt) statementNode() {}
  1. Update visitor (visitor/visitor.go):
func (v *Visitor) EnterNewKeyword(ctx *parser.NewKeywordContext) {
    stmt := &ast.NewKeywordStmt{
        Name: buildQualifiedName(ctx.QualifiedName()),
    }
    v.program.Statements = append(v.program.Statements, stmt)
}
  1. Update executor (executor/executor.go):
func (e *Executor) Execute(stmt ast.Statement) error {
    switch s := stmt.(type) {
    // ... existing cases
    case *ast.NewKeywordStmt:
        return e.executeNewKeyword(s)
    }
}

Microflow Validation

Before execution, mxcli check runs AST-level semantic checks on microflow bodies via ValidateMicroflow(). These checks operate purely on the parsed AST and require no project connection:

  1. Return value consistency – RETURN must provide a value when the microflow declares a return type
  2. Return type plausibility – Scalar literals cannot be returned from entity-typed microflows
  3. Return path coverage – All code paths must end with RETURN for non-void microflows
  4. Variable scope – Variables declared inside IF/ELSE branches cannot be referenced after the branch ends
  5. Validation feedback – VALIDATION FEEDBACK must have a non-empty message template

Handling Nil Values from ANTLR

ANTLR parsers can return partial parse trees with nil nodes when there are syntax errors. Always check if grammar element getters return nil before calling methods on them:

// DANGEROUS - will panic if AttributeName() returns nil
attr.Name = a.AttributeName().GetText()

// SAFE - check for nil first
if a.AttributeName() == nil {
    b.addErrorWithExample(
        "Invalid attribute: each attribute must have a name and type",
        `  CREATE PERSISTENT ENTITY MyModule.Customer (
    Name: String(100) NOT NULL,
    Email: String(200),
    Age: Integer
  );`)
    continue
}
attr.Name = a.AttributeName().GetText()

Common ANTLR context methods that can return nil on parse errors:

  • AttributeName() – missing attribute identifier
  • EnumValueName() – missing enumeration value identifier
  • QualifiedName() – missing or malformed qualified name
  • DataType() – missing type specification
  • Expression() – missing or malformed expression

Adding New Statements

Step-by-step guide for adding a new MDL statement to the parser and executor.

Overview

Adding a new statement requires changes in five layers:

  1. Grammar – lexer tokens and parser rules
  2. AST – typed node struct
  3. Visitor – parse tree to AST conversion
  4. Executor – AST to SDK call
  5. Tests – parser and execution tests

Step 1: Grammar Rules

Add Tokens (if needed)

In mdl/grammar/MDLLexer.g4, add any new keywords:

WORKFLOW : W O R K F L O W ;

Add Parser Rule

In mdl/grammar/MDLParser.g4, add the statement rule:

// Add to the statement alternatives
statement
    : ...
    | createWorkflowStatement
    ;

// Define the new statement
createWorkflowStatement
    : CREATE WORKFLOW qualifiedName
      '(' workflowBody ')'
    ;

workflowBody
    : workflowActivity (workflowActivity)*
    ;

workflowActivity
    : USER_TASK IDENTIFIER STRING_LITERAL
    ;

Regenerate Parser

make grammar

Step 2: AST Node

In mdl/ast/, create or extend an AST file:

// ast/ast_workflow.go
package ast

type CreateWorkflowStmt struct {
    Name       QualifiedName
    Activities []*WorkflowActivity
}

type WorkflowActivity struct {
    Type string
    Name string
    Label string
}

Step 3: Visitor Implementation

In mdl/visitor/visitor.go, implement the listener method:

func (v *MDLVisitor) EnterCreateWorkflowStatement(ctx *parser.CreateWorkflowStatementContext) {
    stmt := &ast.CreateWorkflowStmt{
        Name: v.visitQualifiedName(ctx.QualifiedName()),
    }

    for _, actCtx := range ctx.AllWorkflowActivity() {
        activity := &ast.WorkflowActivity{
            Type:  "UserTask",
            Name:  actCtx.IDENTIFIER().GetText(),
            Label: unquote(actCtx.STRING_LITERAL().GetText()),
        }
        stmt.Activities = append(stmt.Activities, activity)
    }

    v.statements = append(v.statements, stmt)
}

Step 4: Executor Handler

In mdl/executor/, create the execution handler:

// executor/cmd_workflows.go
func (e *Executor) execCreateWorkflow(stmt *ast.CreateWorkflowStmt) error {
    module, err := e.findModule(stmt.Name.Module)
    if err != nil {
        return err
    }

    // Build the workflow using SDK types
    workflow := &workflows.Workflow{
        Name: stmt.Name.Name,
        // ... populate from stmt
    }

    return e.writer.CreateWorkflow(module.ID, workflow)
}

Register the handler in executor.go:

func (e *Executor) Execute(stmt ast.Statement) error {
    switch s := stmt.(type) {
    // ... existing cases
    case *ast.CreateWorkflowStmt:
        return e.execCreateWorkflow(s)
    }
}

Step 5: Tests

Parser Test

Verify the grammar parses correctly:

func TestParseCreateWorkflow(t *testing.T) {
    input := `CREATE WORKFLOW Sales.ApprovalProcess (
        USER_TASK reviewTask 'Review Order'
    );`
    stmts, err := parser.Parse(input)
    require.NoError(t, err)
    require.Len(t, stmts, 1)

    ws := stmts[0].(*ast.CreateWorkflowStmt)
    assert.Equal(t, "Sales", ws.Name.Module)
    assert.Equal(t, "ApprovalProcess", ws.Name.Name)
}

Execution Test

Test against a real MPR file:

func TestExecuteCreateWorkflow(t *testing.T) {
    writer := setupTestProject(t)
    defer writer.Close()

    err := executor.ExecuteString(writer,
        `CREATE WORKFLOW TestModule.MyWorkflow (...);`)
    require.NoError(t, err)
}

Checklist

  • Lexer tokens added (if new keywords)
  • Parser rule added
  • Parser regenerated (make grammar)
  • AST node defined
  • Visitor listener implemented
  • Executor handler registered
  • Parser tests pass
  • Execution tests pass
  • mxcli check validates the new syntax
  • Studio Pro opens the modified project without errors

Catalog System

The catalog is an in-memory SQLite database that indexes Mendix project metadata for fast querying, full-text search, and cross-reference navigation.

Purpose

Reading a Mendix project requires parsing BSON documents from the MPR file. The catalog pre-processes this data into relational tables, enabling:

  • SQL queries against project metadata (SELECT ... FROM CATALOG.ENTITIES)
  • Full-text search across all strings and source (SEARCH 'keyword')
  • Cross-reference tracking (callers, callees, references, impact analysis)
  • Lint rule evaluation (Starlark rules query catalog tables)

Building the Catalog

The catalog is populated by the REFRESH CATALOG command:

-- Quick refresh: basic tables (modules, entities, microflows, pages, etc.)
REFRESH CATALOG;

-- Full refresh: adds cross-references, widgets, and source tables
REFRESH CATALOG FULL;

The full refresh is required for:

  • SHOW CALLERS / SHOW CALLEES / SHOW REFERENCES / SHOW IMPACT
  • SHOW WIDGETS / UPDATE WIDGETS
  • SELECT ... FROM CATALOG.REFS
  • SELECT ... FROM CATALOG.SOURCE

Querying the Catalog

Use standard SQL syntax with the CATALOG. prefix:

-- Find large microflows
SELECT Name, ActivityCount
FROM CATALOG.MICROFLOWS
WHERE ActivityCount > 10
ORDER BY ActivityCount DESC;

-- Find all entity usages
SELECT SourceName, RefKind, TargetName
FROM CATALOG.REFS
WHERE TargetName = 'MyModule.Customer';

-- Search the strings table
SELECT * FROM CATALOG.STRINGS
WHERE strings MATCH 'error'
LIMIT 10;

Available Tables

TableContentsPopulated By
MODULESModule names and IDsREFRESH CATALOG
ENTITIESEntity names, persistence, attribute countsREFRESH CATALOG
MICROFLOWSMicroflow names, activity counts, parametersREFRESH CATALOG
NANOFLOWSNanoflow names and metadataREFRESH CATALOG
PAGESPage names, layouts, URLsREFRESH CATALOG
SNIPPETSSnippet namesREFRESH CATALOG
ENUMERATIONSEnumeration names and value countsREFRESH CATALOG
WORKFLOWSWorkflow names and activity countsREFRESH CATALOG
ACTIVITIESIndividual microflow activitiesREFRESH CATALOG FULL
WIDGETSWidget instances across all pagesREFRESH CATALOG FULL
REFSCross-references between documentsREFRESH CATALOG FULL
PERMISSIONSSecurity permissions (entity access, microflow access)REFRESH CATALOG FULL
STRINGSFTS5 full-text search indexREFRESH CATALOG FULL
SOURCEMDL source text for all documentsREFRESH CATALOG FULL

Implementation

The catalog is implemented in mdl/catalog/catalog.go. It:

  1. Creates an in-memory SQLite database
  2. Iterates all modules and documents in the project
  3. Inserts rows into the appropriate tables
  4. Builds FTS5 indexes for full-text search
  5. Builds the reference graph for cross-reference queries

The catalog lives for the duration of the mxcli session and is discarded on exit.

SQLite Schema

The catalog uses an in-memory SQLite database with the following table definitions.

Core Tables

MODULES

CREATE TABLE MODULES (
    Name        TEXT PRIMARY KEY,
    ModuleID    TEXT,
    SortIndex   INTEGER
);

ENTITIES

CREATE TABLE ENTITIES (
    Name            TEXT PRIMARY KEY,   -- Qualified: Module.Entity
    ModuleName      TEXT,
    EntityName      TEXT,
    Persistent      BOOLEAN,
    AttributeCount  INTEGER,
    Documentation   TEXT
);

MICROFLOWS

CREATE TABLE MICROFLOWS (
    Name            TEXT PRIMARY KEY,   -- Qualified: Module.Microflow
    ModuleName      TEXT,
    MicroflowName   TEXT,
    ActivityCount   INTEGER,
    ParameterCount  INTEGER,
    ReturnType      TEXT,
    Folder          TEXT,
    Documentation   TEXT
);

NANOFLOWS

CREATE TABLE NANOFLOWS (
    Name            TEXT PRIMARY KEY,
    ModuleName      TEXT,
    NanoflowName    TEXT,
    ActivityCount   INTEGER,
    Documentation   TEXT
);

PAGES

CREATE TABLE PAGES (
    Name        TEXT PRIMARY KEY,
    ModuleName  TEXT,
    PageName    TEXT,
    Layout      TEXT,
    Url         TEXT,
    Documentation TEXT
);

SNIPPETS

CREATE TABLE SNIPPETS (
    Name        TEXT PRIMARY KEY,
    ModuleName  TEXT,
    SnippetName TEXT
);

ENUMERATIONS

CREATE TABLE ENUMERATIONS (
    Name        TEXT PRIMARY KEY,
    ModuleName  TEXT,
    EnumName    TEXT,
    ValueCount  INTEGER
);

WORKFLOWS

CREATE TABLE WORKFLOWS (
    Name            TEXT PRIMARY KEY,
    ModuleName      TEXT,
    WorkflowName    TEXT,
    ActivityCount   INTEGER
);

Full Refresh Tables

These tables are only populated by REFRESH CATALOG FULL.

ACTIVITIES

CREATE TABLE ACTIVITIES (
    DocumentName    TEXT,       -- Parent microflow/nanoflow
    ActivityType    TEXT,       -- e.g., "CreateObjectAction", "CallMicroflowAction"
    Caption         TEXT,       -- Activity caption/description
    SortOrder       INTEGER     -- Order within the flow
);

WIDGETS

CREATE TABLE WIDGETS (
    DocumentName    TEXT,       -- Parent page/snippet
    WidgetName      TEXT,       -- Widget instance name
    WidgetType      TEXT,       -- e.g., "Forms$TextBox", "CustomWidgets$ComboBox"
    ModuleName      TEXT
);

REFS

CREATE TABLE REFS (
    SourceName  TEXT,           -- Referencing document
    SourceKind  TEXT,           -- "Microflow", "Page", etc.
    TargetName  TEXT,           -- Referenced element
    TargetKind  TEXT,           -- "Entity", "Microflow", etc.
    RefKind     TEXT            -- "Call", "DataSource", "Association", etc.
);

CREATE INDEX idx_refs_source ON REFS(SourceName);
CREATE INDEX idx_refs_target ON REFS(TargetName);

PERMISSIONS

CREATE TABLE PERMISSIONS (
    RoleName    TEXT,           -- Module role
    TargetName  TEXT,           -- Entity, microflow, or page
    TargetKind  TEXT,           -- "Entity", "Microflow", "Page"
    Permission  TEXT            -- "Create", "Read", "Write", "Delete", "Execute", "View"
);

Full-Text Search Tables

STRINGS (FTS5)

CREATE VIRTUAL TABLE STRINGS USING fts5(
    name,           -- Document qualified name
    kind,           -- Document type
    strings,        -- All text content concatenated
    tokenize='porter unicode61'
);

SOURCE (FTS5)

CREATE VIRTUAL TABLE SOURCE USING fts5(
    name,           -- Document qualified name
    kind,           -- Document type
    source,         -- MDL source representation
    tokenize='porter unicode61'
);

Querying Examples

-- Find entities with many attributes
SELECT Name, AttributeCount FROM CATALOG.ENTITIES
WHERE AttributeCount > 20 ORDER BY AttributeCount DESC;

-- Find all references to an entity
SELECT SourceName, RefKind FROM CATALOG.REFS
WHERE TargetName = 'Sales.Customer';

-- Full-text search
SELECT name, kind, snippet(STRINGS, 2, '<b>', '</b>', '...', 20)
FROM CATALOG.STRINGS WHERE strings MATCH 'validation error';

FTS5 Full-Text Search

The catalog uses SQLite’s FTS5 extension to provide fast full-text search across all project content.

Overview

Two FTS5 virtual tables are created during REFRESH CATALOG FULL:

TableIndexesUse Case
STRINGSValidation messages, captions, labels, log messagesSEARCH 'keyword'
SOURCEMDL source representation of all documentsCode search

Both use the porter unicode61 tokenizer, which provides:

  • Porter stemming – matches word variants (e.g., “validate” matches “validation”)
  • Unicode support – handles non-ASCII characters correctly

Building the Index

During REFRESH CATALOG FULL, the catalog:

  1. Iterates all documents in the project
  2. Extracts all text content (messages, captions, names, expressions)
  3. Generates the MDL source representation for each document
  4. Inserts into the FTS5 tables

Querying

From MDL

-- Simple keyword search
SEARCH 'validation';

-- Phrase search
SEARCH 'error message';

From SQL

-- Basic match
SELECT name, kind FROM CATALOG.STRINGS
WHERE strings MATCH 'customer';

-- Phrase match
SELECT name, kind FROM CATALOG.STRINGS
WHERE strings MATCH '"email validation"';

-- Boolean operators
SELECT name, kind FROM CATALOG.STRINGS
WHERE strings MATCH 'customer AND NOT deleted';

-- With snippets (highlighted context)
SELECT name, kind,
    snippet(STRINGS, 2, '>>>', '<<<', '...', 20) as context
FROM CATALOG.STRINGS
WHERE strings MATCH 'error'
LIMIT 10;

-- Search MDL source
SELECT name, kind FROM CATALOG.SOURCE
WHERE source MATCH 'RETRIEVE FROM';

FTS5 Query Syntax

FTS5 supports a rich query syntax:

SyntaxMeaning
wordMatch documents containing “word”
"exact phrase"Match exact phrase
word1 AND word2Both words must appear
word1 OR word2Either word may appear
NOT wordExclude documents with this word
word*Prefix match (e.g., “valid*” matches “validation”)
NEAR(word1 word2, 5)Words within 5 tokens of each other

CLI Integration

The SEARCH command and mxcli search subcommand are wrappers around FTS5 queries:

# Search from command line
mxcli search -p app.mpr "validation"

# Pipe-friendly output
mxcli search -p app.mpr "error" -q --format names

# JSON output
mxcli search -p app.mpr "Customer" -q --format json

The search results include:

  • Document type (Microflow, Page, Entity, etc.)
  • Qualified name
  • Matching context snippet

Performance

FTS5 indexes are built in memory and provide sub-millisecond query times for typical project sizes. The index construction itself takes a few seconds for large projects (1000+ documents), which is why it requires the explicit REFRESH CATALOG FULL command.

Reference Tracking

The cross-reference tracking system powers the callers, callees, references, and impact analysis queries. It is built during REFRESH CATALOG FULL and stored in the REFS table.

How References Are Collected

During a full catalog refresh, every document is analyzed for outgoing references:

Document TypeReference Sources
MicroflowsCALL MICROFLOW actions, RETRIEVE data sources, entity parameters, SHOW PAGE actions, association traversals
NanoflowsSame as microflows (client-side)
PagesData source entities, microflow data sources, SHOW_PAGE actions, association paths, snippet calls
SnippetsSame as pages
Domain ModelsGeneralization references, association endpoints

Each reference is stored as a row in the REFS table:

INSERT INTO REFS (SourceName, SourceKind, TargetName, TargetKind, RefKind)
VALUES ('Sales.ProcessOrder', 'Microflow', 'Sales.Customer', 'Entity', 'Retrieve');

Query Commands

SHOW CALLERS

Find what calls a given element:

SHOW CALLERS OF Sales.ProcessOrder;

Returns all microflows, nanoflows, and pages that reference Sales.ProcessOrder.

Transitive callers follow the call chain recursively:

SHOW CALLERS OF Sales.ProcessOrder TRANSITIVE;

SHOW CALLEES

Find what a microflow calls:

SHOW CALLEES OF Sales.ProcessOrder;

Returns all microflows, entities, and pages referenced by Sales.ProcessOrder.

SHOW REFERENCES

Find all references to an element:

SHOW REFERENCES TO Sales.Customer;

Returns every document that references Sales.Customer in any way (data source, parameter, association, etc.).

SHOW IMPACT

Analyze the impact of changing an element:

SHOW IMPACT OF Sales.Customer;

Returns all direct and transitive dependents – everything that would potentially be affected by changing or removing the element.

SHOW CONTEXT

Assemble context for understanding a document:

SHOW CONTEXT OF Sales.ProcessOrder DEPTH 3;

Returns the element itself plus its callers and callees up to the specified depth, providing a focused view of the element’s neighborhood in the dependency graph.

CLI Commands

The same queries are available as CLI subcommands:

# Direct callers
mxcli callers -p app.mpr Sales.ProcessOrder

# Transitive callers
mxcli callers -p app.mpr Sales.ProcessOrder --transitive

# Callees
mxcli callees -p app.mpr Sales.ProcessOrder

# All references
mxcli refs -p app.mpr Sales.Customer

# Impact analysis
mxcli impact -p app.mpr Sales.Customer

# Context assembly
mxcli context -p app.mpr Sales.ProcessOrder --depth 3

Reference Kinds

The RefKind column in the REFS table categorizes the relationship:

RefKindMeaning
CallMicroflow/nanoflow call action
RetrieveDatabase retrieve with entity data source
DataSourcePage/widget data source reference
ShowPageShow page action
ParameterMicroflow parameter type reference
AssociationAssociation traversal
GeneralizationEntity generalization (inheritance)
SnippetCallSnippet embedded in a page
EnumerationEnumeration type reference

Implementation

The reference tracker is implemented in the catalog package (mdl/catalog/catalog.go). It:

  1. Parses each document’s BSON structure
  2. Extracts all qualified name references
  3. Classifies each reference by kind
  4. Inserts into the REFS table with source/target/kind

Transitive queries use recursive SQL CTEs (Common Table Expressions) to follow reference chains.

MDL Quick Reference

Complete syntax reference for MDL (Mendix Definition Language). This is the authoritative reference for all MDL statement syntax.

Entity Generalization (EXTENDS)

CRITICAL: EXTENDS goes BEFORE the opening parenthesis, not after!

-- Correct: EXTENDS before (
CREATE PERSISTENT ENTITY Module.ProductPhoto EXTENDS System.Image (
  PhotoCaption: String(200)
);

-- Wrong: EXTENDS after ) = parse error!
CREATE PERSISTENT ENTITY Module.Photo (
  PhotoCaption: String(200)
) EXTENDS System.Image;

Domain Model

StatementSyntaxNotes
Create entityCREATE [OR MODIFY] PERSISTENT|NON-PERSISTENT ENTITY Module.Name (attrs);Persistent is default
Create with extendsCREATE PERSISTENT ENTITY Module.Name EXTENDS Parent.Entity (attrs);EXTENDS before (
Create view entityCREATE VIEW ENTITY Module.Name (attrs) AS SELECT ...;OQL-backed read-only
Create external entityCREATE EXTERNAL ENTITY Module.Name FROM ODATA CLIENT Module.Client (...) (attrs);From consumed OData
Drop entityDROP ENTITY Module.Name;
Describe entityDESCRIBE ENTITY Module.Name;Full MDL output
Show entitiesSHOW ENTITIES [IN Module];List all or filter by module
Create enumerationCREATE [OR MODIFY] ENUMERATION Module.Name (Value1 'Caption', ...);
Drop enumerationDROP ENUMERATION Module.Name;
Create associationCREATE ASSOCIATION Module.Name FROM Parent TO Child TYPE Reference|ReferenceSet [OWNER Default|Both] [DELETE_BEHAVIOR ...];
Drop associationDROP ASSOCIATION Module.Name;

ALTER ENTITY

Modifies an existing entity without full replacement.

OperationSyntaxNotes
Add attributesALTER ENTITY Module.Name ADD (Attr: Type [constraints]);One or more attributes
Drop attributesALTER ENTITY Module.Name DROP (AttrName, ...);
Modify attributesALTER ENTITY Module.Name MODIFY (Attr: NewType [constraints]);Change type/constraints
Rename attributeALTER ENTITY Module.Name RENAME OldName TO NewName;
Add indexALTER ENTITY Module.Name ADD INDEX (Col1 [ASC|DESC], ...);
Drop indexALTER ENTITY Module.Name DROP INDEX (Col1, ...);
Set documentationALTER ENTITY Module.Name SET DOCUMENTATION 'text';

Example:

ALTER ENTITY Sales.Customer
  ADD (Phone: String(50), Notes: String(unlimited));

ALTER ENTITY Sales.Customer
  RENAME Phone TO PhoneNumber;

ALTER ENTITY Sales.Customer
  ADD INDEX (Email);

Constants

StatementSyntaxNotes
Show constantsSHOW CONSTANTS [IN Module];List all or filter by module
Describe constantDESCRIBE CONSTANT Module.Name;Full MDL output
Create constantCREATE [OR MODIFY] CONSTANT Module.Name TYPE DataType DEFAULT 'value';String, Integer, Boolean, etc.
Drop constantDROP CONSTANT Module.Name;

Example:

CREATE CONSTANT MyModule.ApiBaseUrl TYPE String DEFAULT 'https://api.example.com';
CREATE CONSTANT MyModule.MaxRetries TYPE Integer DEFAULT 3;
CREATE CONSTANT MyModule.EnableLogging TYPE Boolean DEFAULT true;

OData Clients, Services & External Entities

StatementSyntaxNotes
Show OData clientsSHOW ODATA CLIENTS [IN Module];Consumed OData services
Describe OData clientDESCRIBE ODATA CLIENT Module.Name;Full MDL output
Create OData clientCREATE [OR MODIFY] ODATA CLIENT Module.Name (...);Version, MetadataUrl, Timeout, etc.
Alter OData clientALTER ODATA CLIENT Module.Name SET Key = Value;
Drop OData clientDROP ODATA CLIENT Module.Name;
Show OData servicesSHOW ODATA SERVICES [IN Module];Published OData services
Describe OData serviceDESCRIBE ODATA SERVICE Module.Name;Full MDL output
Create OData serviceCREATE [OR MODIFY] ODATA SERVICE Module.Name (...) AUTHENTICATION ... { PUBLISH ENTITY ... };
Alter OData serviceALTER ODATA SERVICE Module.Name SET Key = Value;
Drop OData serviceDROP ODATA SERVICE Module.Name;
Show external entitiesSHOW EXTERNAL ENTITIES [IN Module];OData-backed entities
Create external entityCREATE [OR MODIFY] EXTERNAL ENTITY Module.Name FROM ODATA CLIENT Module.Client (...) (attrs);
Grant OData accessGRANT ACCESS ON ODATA SERVICE Module.Name TO Module.Role, ...;
Revoke OData accessREVOKE ACCESS ON ODATA SERVICE Module.Name FROM Module.Role, ...;

OData Client Example:

CREATE ODATA CLIENT MyModule.ExternalAPI (
  Version: '1.0',
  ODataVersion: OData4,
  MetadataUrl: 'https://api.example.com/odata/v4/$metadata',
  Timeout: 300
);

OData Service Example:

CREATE ODATA SERVICE MyModule.CustomerAPI (
  Path: '/odata/customers',
  Version: '1.0.0',
  ODataVersion: OData4,
  Namespace: 'MyModule.Customers'
)
AUTHENTICATION Basic, Session
{
  PUBLISH ENTITY MyModule.Customer AS 'Customers' (
    ReadMode: SOURCE,
    InsertMode: SOURCE,
    UpdateMode: NOT_SUPPORTED,
    DeleteMode: NOT_SUPPORTED,
    UsePaging: Yes,
    PageSize: 100
  )
  EXPOSE (Name, Email, Phone);
};

Microflows - Supported Statements

StatementSyntaxNotes
Variable declarationDECLARE $Var Type = value;Primitives: String, Integer, Boolean, Decimal, DateTime
Entity declarationDECLARE $Entity Module.Entity;No AS keyword, no = empty
List declarationDECLARE $List List of Module.Entity = empty;
AssignmentSET $Var = expression;Variable must be declared first
Create object$Var = CREATE Module.Entity (Attr = value);
Change objectCHANGE $Entity (Attr = value);
CommitCOMMIT $Entity [WITH EVENTS] [REFRESH];
DeleteDELETE $Entity;
RollbackROLLBACK $Entity [REFRESH];Reverts uncommitted changes
Retrieve (DB)RETRIEVE $Var FROM Module.Entity [WHERE condition];Database XPath retrieve
Retrieve (Assoc)RETRIEVE $List FROM $Parent/Module.AssocName;Retrieve by association
Call microflow$Result = CALL MICROFLOW Module.Name (Param = $value);
Call nanoflow$Result = CALL NANOFLOW Module.Name (Param = $value);
Show pageSHOW PAGE Module.PageName ($Param = $value);Also accepts (Param: $value)
Close pageCLOSE PAGE;
ValidationVALIDATION FEEDBACK $Entity/Attribute MESSAGE 'message';Requires attribute path + MESSAGE
LogLOG INFO|WARNING|ERROR [NODE 'name'] 'message';
Position@position(x, y)Canvas position (before activity)
Caption@caption 'text'Custom caption (before activity)
Color@color GreenBackground color (before activity)
Annotation@annotation 'text'Visual note attached to next activity
IFIF condition THEN ... [ELSE ...] END IF;
LOOPLOOP $Item IN $List BEGIN ... END LOOP;FOR EACH over list
WHILEWHILE condition BEGIN ... END WHILE;Condition-based loop
ReturnRETURN $value;Required at end of every flow path
Execute DB query$Result = EXECUTE DATABASE QUERY Module.Conn.Query;3-part name; supports DYNAMIC, params, CONNECTION override
Error handling... ON ERROR CONTINUE|ROLLBACK|{ handler };Not supported on EXECUTE DATABASE QUERY

Microflows - NOT Supported (Will Cause Parse Errors)

UnsupportedUse InsteadNotes
CASE ... WHEN ... END CASENested IF ... ELSE ... END IFSwitch not implemented
TRY ... CATCH ... END TRYON ERROR { ... } blocksUse error handlers on specific activities

Notes:

  • RETRIEVE ... LIMIT n IS supported. LIMIT 1 returns a single entity, otherwise returns a list.
  • ROLLBACK $Entity [REFRESH]; IS supported. Rolls back uncommitted changes to an object.

Project Organization

StatementSyntaxNotes
Microflow folderFOLDER 'path' (before BEGIN)CREATE MICROFLOW ... FOLDER 'ACT' BEGIN ... END;
Page folderFolder: 'path' (in properties)CREATE PAGE ... (Folder: 'Pages/Detail') { ... }
Move to folderMOVE PAGE|MICROFLOW|SNIPPET|NANOFLOW|ENUMERATION Module.Name TO FOLDER 'path';Folders created automatically
Move to module rootMOVE PAGE Module.Name TO Module;Removes from folder
Move across modulesMOVE PAGE Old.Name TO NewModule;Breaks by-name references – use SHOW IMPACT OF first
Move to folder in other moduleMOVE PAGE Old.Name TO FOLDER 'path' IN NewModule;
Move entity to moduleMOVE ENTITY Old.Name TO NewModule;Entities don’t support folders

Nested folders use / separator: 'Parent/Child/Grandchild'. Missing folders are auto-created.

Security Management

StatementSyntaxNotes
Show project securitySHOW PROJECT SECURITY;Displays security level, admin, demo users
Show module rolesSHOW MODULE ROLES [IN Module];All roles or filtered by module
Show user rolesSHOW USER ROLES;Project-level user roles
Show demo usersSHOW DEMO USERS;Configured demo users
Show access on elementSHOW ACCESS ON MICROFLOW|PAGE|Entity Mod.Name;Which roles can access
Show security matrixSHOW SECURITY MATRIX [IN Module];Full access overview
Create module roleCREATE MODULE ROLE Mod.Role [DESCRIPTION 'text'];
Drop module roleDROP MODULE ROLE Mod.Role;
Create user roleCREATE USER ROLE Name (Mod.Role, ...) [MANAGE ALL ROLES];Aggregates module roles
Alter user roleALTER USER ROLE Name ADD|REMOVE MODULE ROLES (Mod.Role, ...);
Drop user roleDROP USER ROLE Name;
Grant microflow accessGRANT EXECUTE ON MICROFLOW Mod.MF TO Mod.Role, ...;
Revoke microflow accessREVOKE EXECUTE ON MICROFLOW Mod.MF FROM Mod.Role, ...;
Grant page accessGRANT VIEW ON PAGE Mod.Page TO Mod.Role, ...;
Revoke page accessREVOKE VIEW ON PAGE Mod.Page FROM Mod.Role, ...;
Grant entity accessGRANT Mod.Role ON Mod.Entity (CREATE, DELETE, READ *, WRITE *);Supports member lists and WHERE
Revoke entity accessREVOKE Mod.Role ON Mod.Entity;
Set security levelALTER PROJECT SECURITY LEVEL OFF|PROTOTYPE|PRODUCTION;
Toggle demo usersALTER PROJECT SECURITY DEMO USERS ON|OFF;
Create demo userCREATE DEMO USER 'name' PASSWORD 'pass' [ENTITY Module.Entity] (UserRole, ...);
Drop demo userDROP DEMO USER 'name';

Workflows

StatementSyntaxNotes
Show workflowsSHOW WORKFLOWS [IN Module];List all or filter by module
Describe workflowDESCRIBE WORKFLOW Module.Name;Full MDL output
Create workflowCREATE [OR MODIFY] WORKFLOW Module.Name PARAMETER $Ctx: Module.Entity BEGIN ... END WORKFLOW;See activity types below
Drop workflowDROP WORKFLOW Module.Name;
Grant workflow accessGRANT EXECUTE ON WORKFLOW Module.Name TO Mod.Role, ...;
Revoke workflow accessREVOKE EXECUTE ON WORKFLOW Module.Name FROM Mod.Role, ...;

Workflow Activity Types:

  • USER TASK <name> '<caption>' [PAGE Mod.Page] [TARGETING MICROFLOW Mod.MF] [OUTCOMES '<out>' { } ...];
  • CALL MICROFLOW Mod.MF [COMMENT '<text>'] [OUTCOMES '<out>' { } ...];
  • CALL WORKFLOW Mod.WF [COMMENT '<text>'];
  • DECISION ['<caption>'] OUTCOMES '<out>' { } ...;
  • PARALLEL SPLIT PATH 1 { } PATH 2 { };
  • JUMP TO <activity-name>;
  • WAIT FOR TIMER ['<expr>'];
  • WAIT FOR NOTIFICATION;
  • END;

Example:

CREATE WORKFLOW Module.ApprovalFlow
  PARAMETER $Context: Module.Request
  OVERVIEW PAGE Module.WorkflowOverview
BEGIN
  USER TASK ReviewTask 'Review the request'
    PAGE Module.ReviewPage
    OUTCOMES 'Approve' { } 'Reject' { };
END WORKFLOW;

Project Structure

StatementSyntaxNotes
Structure overviewSHOW STRUCTURE;Depth 2 (elements with signatures), user modules only
Module countsSHOW STRUCTURE DEPTH 1;One line per module with element counts
Full typesSHOW STRUCTURE DEPTH 3;Typed attributes, named parameters
Filter by moduleSHOW STRUCTURE IN ModuleName;Single module only
Include all modulesSHOW STRUCTURE DEPTH 1 ALL;Include system/marketplace modules
StatementSyntaxNotes
Show navigationSHOW NAVIGATION;Summary of all profiles
Show menu treeSHOW NAVIGATION MENU [Profile];Menu tree for profile or all
Show home pagesSHOW NAVIGATION HOMES;Home page assignments across profiles
Describe navigationDESCRIBE NAVIGATION [Profile];Full MDL output (round-trippable)
Create/replace navigationCREATE OR REPLACE NAVIGATION Profile ...;Full replacement of profile

Navigation Example:

CREATE OR REPLACE NAVIGATION Responsive
  HOME PAGE MyModule.Home_Web
  HOME PAGE MyModule.AdminHome FOR MyModule.Administrator
  LOGIN PAGE Administration.Login
  NOT FOUND PAGE MyModule.Custom404
  MENU (
    MENU ITEM 'Home' PAGE MyModule.Home_Web;
    MENU 'Admin' (
      MENU ITEM 'Users' PAGE Administration.Account_Overview;
    );
  );

Project Settings

StatementSyntaxNotes
Show settingsSHOW SETTINGS;Overview of all settings parts
Describe settingsDESCRIBE SETTINGS;Full MDL output (round-trippable)
Alter model settingsALTER SETTINGS MODEL Key = Value;AfterStartupMicroflow, HashAlgorithm, JavaVersion, etc.
Alter configurationALTER SETTINGS CONFIGURATION 'Name' Key = Value;DatabaseType, DatabaseUrl, HttpPortNumber, etc.
Alter constantALTER SETTINGS CONSTANT 'Name' VALUE 'val' IN CONFIGURATION 'cfg';Override constant per configuration
Alter languageALTER SETTINGS LANGUAGE Key = Value;DefaultLanguageCode
Alter workflowsALTER SETTINGS WORKFLOWS Key = Value;UserEntity, DefaultTaskParallelism

Business Events

StatementSyntaxNotes
Show servicesSHOW BUSINESS EVENTS;List all business event services
Show in moduleSHOW BUSINESS EVENTS IN Module;Filter by module
Describe serviceDESCRIBE BUSINESS EVENT SERVICE Module.Name;Full MDL output
Create serviceCREATE BUSINESS EVENT SERVICE Module.Name (...) { MESSAGE ... };See help topic for full syntax
Drop serviceDROP BUSINESS EVENT SERVICE Module.Name;Delete a service

Java Actions

StatementSyntaxNotes
Show Java actionsSHOW JAVA ACTIONS [IN Module];List all or filtered by module
Describe Java actionDESCRIBE JAVA ACTION Module.Name;Full MDL output with signature
Create Java actionCREATE JAVA ACTION Module.Name(params) RETURNS type AS $$ ... $$;Inline Java code
Create with type paramsCREATE JAVA ACTION Module.Name(EntityType: ENTITY <pEntity>, Obj: pEntity) ...;Generic type parameters
Create exposed action... EXPOSED AS 'Caption' IN 'Category' AS $$ ... $$;Toolbox-visible in Studio Pro
Drop Java actionDROP JAVA ACTION Module.Name;Delete a Java action
Call from microflow$Result = CALL JAVA ACTION Module.Name(Param = value);Inside BEGIN…END

Parameter Types: String, Integer, Long, Decimal, Boolean, DateTime, Module.Entity, List of Module.Entity, ENUM Module.EnumName, Enumeration(Module.EnumName), StringTemplate(Sql), StringTemplate(Oql), ENTITY <pEntity> (type parameter declaration), bare pEntity (type parameter reference).

Type Parameters allow generic entity handling. ENTITY <pEntity> declares the type parameter inline and becomes the entity type selector; bare pEntity parameters receive entity instances:

CREATE JAVA ACTION Module.Validate(
  EntityType: ENTITY <pEntity> NOT NULL,
  InputObject: pEntity NOT NULL
) RETURNS Boolean
EXPOSED AS 'Validate Entity' IN 'Validation'
AS $$
return InputObject != null;
$$;

Pages

MDL uses explicit property declarations for pages:

ElementSyntaxExample
Page properties(Key: value, ...)(Title: 'Edit', Layout: Atlas_Core.Atlas_Default)
Page variablesVariables: { $name: Type = 'expr' }Variables: { $show: Boolean = 'true' }
Widget nameRequired after typeTEXTBOX txtName (...)
Attribute bindingAttribute: AttrNameTEXTBOX txt (Label: 'Name', Attribute: Name)
Variable bindingDataSource: $VarDATAVIEW dv (DataSource: $Product) { ... }
Action bindingAction: TYPEACTIONBUTTON btn (Caption: 'Save', Action: SAVE_CHANGES)
Microflow actionAction: MICROFLOW Name(Param: val)Action: MICROFLOW Mod.ACT_Process(Order: $Order)
Database sourceDataSource: DATABASE EntityDATAGRID dg (DataSource: DATABASE Module.Entity)
Selection bindingDataSource: SELECTION widgetDATAVIEW dv (DataSource: SELECTION galleryList)
CSS classClass: 'classes'CONTAINER c (Class: 'card mx-spacing-top-large')
Inline styleStyle: 'css'CONTAINER c (Style: 'padding: 16px;')
Design propertiesDesignProperties: [...]CONTAINER c (DesignProperties: ['Spacing top': 'Large', 'Full width': ON])
Width (pixels)Width: integerIMAGE img (Width: 200)
Height (pixels)Height: integerIMAGE img (Height: 150)
Page sizePageSize: integerDATAGRID dg (PageSize: 25)
Pagination modePagination: modeDATAGRID dg (Pagination: virtualScrolling)
Paging positionPagingPosition: posDATAGRID dg (PagingPosition: both)
Paging buttonsShowPagingButtons: modeDATAGRID dg (ShowPagingButtons: auto)

DataGrid Column Properties:

PropertyValuesDefaultExample
Attributeattribute name(required)Attribute: Price
Captionstringattribute nameCaption: 'Unit Price'
Alignmentleft, center, rightleftAlignment: right
WrapTexttrue, falsefalseWrapText: true
Sortabletrue, falsetrue/falseSortable: false
Resizabletrue, falsetrueResizable: false
Draggabletrue, falsetrueDraggable: false
Hidableyes, hidden, noyesHidable: no
ColumnWidthautoFill, autoFit, manualautoFillColumnWidth: manual
Sizeinteger (px)1Size: 200
Visibleexpression stringtrueVisible: '$showColumn' (page variable, not $currentObject)
DynamicCellClassexpression string(empty)DynamicCellClass: 'if(...) then ... else ...'
Tooltiptext string(empty)Tooltip: 'Price in USD'

Page Example:

CREATE PAGE MyModule.Customer_Edit
(
  Params: { $Customer: MyModule.Customer },
  Title: 'Edit Customer',
  Layout: Atlas_Core.PopupLayout
)
{
  DATAVIEW dvCustomer (DataSource: $Customer) {
    TEXTBOX txtName (Label: 'Name', Attribute: Name)
    TEXTBOX txtEmail (Label: 'Email', Attribute: Email)
    COMBOBOX cbStatus (Label: 'Status', Attribute: Status)

    FOOTER footer1 {
      ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)
      ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES)
    }
  }
}

Supported Widgets:

  • Layout: LAYOUTGRID, ROW, COLUMN, CONTAINER, CUSTOMCONTAINER
  • Input: TEXTBOX, TEXTAREA, CHECKBOX, RADIOBUTTONS, DATEPICKER, COMBOBOX
  • Display: DYNAMICTEXT, DATAGRID, GALLERY, LISTVIEW, IMAGE, STATICIMAGE, DYNAMICIMAGE
  • Actions: ACTIONBUTTON, LINKBUTTON, NAVIGATIONLIST
  • Structure: DATAVIEW, HEADER, FOOTER, CONTROLBAR, SNIPPETCALL

ALTER PAGE / ALTER SNIPPET

Modify an existing page or snippet’s widget tree in-place without full CREATE OR REPLACE. Works directly on the raw BSON tree, preserving unsupported widget types.

OperationSyntaxNotes
Set propertySET Caption = 'New' ON widgetNameSingle property on a widget
Set multipleSET (Caption = 'Save', ButtonStyle = Success) ON btnMultiple properties at once
Page-level setSET Title = 'New Title'No ON clause for page properties
Insert afterINSERT AFTER widgetName { widgets }Add widgets after target
Insert beforeINSERT BEFORE widgetName { widgets }Add widgets before target
Drop widgetsDROP WIDGET name1, name2Remove widgets by name
Replace widgetREPLACE widgetName WITH { widgets }Replace widget subtree
Pluggable propSET 'showLabel' = false ON cbStatusQuoted name for pluggable widgets
Add variableADD Variables $name: Type = 'expr'Add a page variable
Drop variableDROP Variables $nameRemove a page variable

Supported SET properties: Caption, Label, ButtonStyle, Class, Style, Editable, Visible, Name, Title (page-level), and quoted pluggable widget properties.

Example:

ALTER PAGE Module.EditPage {
  SET (Caption = 'Save & Close', ButtonStyle = Success) ON btnSave;
  DROP WIDGET txtUnused;
  INSERT AFTER txtEmail {
    TEXTBOX txtPhone (Label: 'Phone', Attribute: Phone)
  }
};

ALTER SNIPPET Module.NavMenu {
  SET Caption = 'Dashboard' ON btnHome
};

Tip: Run DESCRIBE PAGE Module.PageName first to see widget names.

External SQL Statements

Direct SQL query execution against external databases (PostgreSQL, Oracle, SQL Server). Credentials are isolated – DSN never appears in session output or logs.

StatementSyntaxNotes
ConnectSQL CONNECT <driver> '<dsn>' AS <alias>;Drivers: postgres, oracle, sqlserver
DisconnectSQL DISCONNECT <alias>;Closes connection
List connectionsSQL CONNECTIONS;Shows alias + driver only (no DSN)
Show tablesSQL <alias> SHOW TABLES;Lists user tables
Show viewsSQL <alias> SHOW VIEWS;Lists user views
Show functionsSQL <alias> SHOW FUNCTIONS;Lists functions and procedures
Describe tableSQL <alias> DESCRIBE <table>;Shows columns, types, nullability
QuerySQL <alias> <any-sql>;Raw SQL passthrough
ImportIMPORT FROM <alias> QUERY '<sql>' INTO Module.Entity MAP (...) [LINK (...)] [BATCH n] [LIMIT n];Insert external data into Mendix app DB
Generate connectorSQL <alias> GENERATE CONNECTOR INTO <module> [TABLES (...)] [VIEWS (...)] [EXEC];Generate Database Connector MDL from schema
-- Connect to PostgreSQL
SQL CONNECT postgres 'postgres://user:pass@localhost:5432/mydb' AS source;

-- Explore schema
SQL source SHOW TABLES;
SQL source DESCRIBE users;

-- Query data
SQL source SELECT * FROM users WHERE active = true LIMIT 10;

-- Import external data into Mendix app database
IMPORT FROM source QUERY 'SELECT name, email FROM employees'
  INTO HRModule.Employee
  MAP (name AS Name, email AS Email);

-- Import with association linking
IMPORT FROM source QUERY 'SELECT name, dept_name FROM employees'
  INTO HR.Employee
  MAP (name AS Name)
  LINK (dept_name TO Employee_Department ON Name);

-- Generate Database Connector from schema
SQL source GENERATE CONNECTOR INTO HRModule;
SQL source GENERATE CONNECTOR INTO HRModule TABLES (employees, departments) EXEC;

-- Manage connections
SQL CONNECTIONS;
SQL DISCONNECT source;

CLI subcommand: mxcli sql --driver postgres --dsn '...' "SELECT 1" (see mxcli syntax sql). Supported drivers: postgres (pg, postgresql), oracle (ora), sqlserver (mssql).

StatementSyntaxNotes
Refresh catalogREFRESH CATALOG;Rebuild basic metadata tables
Refresh with refsREFRESH CATALOG FULL;Include cross-references and source
Show catalog tablesSHOW CATALOG TABLES;List available queryable tables
Query catalogSELECT ... FROM CATALOG.<table> [WHERE ...];SQL against project metadata
Show callersSHOW CALLERS OF Module.Name;What calls this element
Show calleesSHOW CALLEES OF Module.Name;What this element calls
Show referencesSHOW REFERENCES OF Module.Name;All references to/from
Show impactSHOW IMPACT OF Module.Name;Impact analysis
Show contextSHOW CONTEXT OF Module.Name;Surrounding context
Full-text searchSEARCH '<keyword>';Search across all strings and source

Cross-reference commands require REFRESH CATALOG FULL to populate reference data.

Connection & Session

StatementSyntaxNotes
ConnectCONNECT LOCAL '/path/to/app.mpr';Open a Mendix project
DisconnectDISCONNECT;Close current project
StatusSTATUS;Show connection info
RefreshREFRESH;Reload project from disk
CommitCOMMIT [MESSAGE 'text'];Save changes to MPR
Set variableSET key = value;Session variable (e.g., output_format = 'json')
ExitEXIT;Close REPL session

CLI Commands

CommandSyntaxNotes
Interactive REPLmxcliInteractive MDL shell
Execute commandmxcli -p app.mpr -c "SHOW ENTITIES"Single command
Execute scriptmxcli exec script.mdl -p app.mprScript file
Check syntaxmxcli check script.mdlParse-only validation
Check referencesmxcli check script.mdl -p app.mpr --referencesWith reference validation
Lint projectmxcli lint -p app.mpr [--format json|sarif]14 built-in + 27 Starlark rules
Reportmxcli report -p app.mpr [--format markdown|json|html]Best practices report
Testmxcli test tests/ -p app.mpr.test.mdl / .test.md files
Diff scriptmxcli diff -p app.mpr changes.mdlCompare script vs project
Diff localmxcli diff-local -p app.mpr --ref HEADGit diff for MPR v2
OQLmxcli oql -p app.mpr "SELECT ..."Query running Mendix runtime
External SQLmxcli sql --driver postgres --dsn '...' "SELECT 1"Direct database query
Docker buildmxcli docker build -p app.mprBuild with PAD patching
Docker checkmxcli docker check -p app.mprValidate with mx check
Diagnosticsmxcli diag [--bundle]Session logs, version info
Init projectmxcli init -p app.mprCreate .claude/ folder with skills
LSP servermxcli lsp --stdioLanguage server for VS Code

Data Type Mapping Table

Comprehensive mapping between MDL data types, Mendix internal types, and backend representations.

Primitive Types

MDL TypeDescriptionRange/Limits
StringVariable-length text (default length 200)1 to unlimited characters
String(n)Variable-length text with explicit length1 to unlimited characters
Integer32-bit signed integer-2,147,483,648 to 2,147,483,647
Long64-bit signed integer-9,223,372,036,854,775,808 to 9,223,372,036,854,775,807
DecimalHigh-precision decimal numberUp to 20 digits total
BooleanTrue/false valuetrue or false (must have DEFAULT)
DateTimeDate and time with timezone awarenessUTC timestamp
DateDate only (no time component)Internally stored as DateTime
AutoNumberAuto-incrementing integerTypically combined with NOT NULL UNIQUE
BinaryBinary data (files, images)Configurable max size
HashedStringSecurely hashed string (passwords)One-way hash, cannot be retrieved

Complex Types

MDL TypeDescriptionExample
Enumeration(Module.EnumName)Reference to an enumeration typeStatus: Enumeration(Sales.OrderStatus)

MDL to Backend Type Mapping

MDL TypeBSON $TypeGo Type (SDK)
StringDomainModels$StringAttributeType*StringAttributeType
String(n)DomainModels$StringAttributeType + Length*StringAttributeType{Length: n}
IntegerDomainModels$IntegerAttributeType*IntegerAttributeType
LongDomainModels$LongAttributeType*LongAttributeType
DecimalDomainModels$DecimalAttributeType*DecimalAttributeType
BooleanDomainModels$BooleanAttributeType*BooleanAttributeType
DateTimeDomainModels$DateTimeAttributeType*DateTimeAttributeType
DateDomainModels$DateTimeAttributeType*DateTimeAttributeType
AutoNumberDomainModels$AutoNumberAttributeType*AutoNumberAttributeType
BinaryDomainModels$BinaryAttributeType*BinaryAttributeType
EnumerationDomainModels$EnumerationAttributeType*EnumerationAttributeType
HashedStringDomainModels$HashedStringAttributeType*HashedStringAttributeType

Default Value Mapping

MDL DefaultBSON StructureGo Structure
DEFAULT 'text'Value: {$Type: "DomainModels$StoredValue", DefaultValue: "text"}Value: &AttributeValue{DefaultValue: "text"}
DEFAULT 123Value: {$Type: "DomainModels$StoredValue", DefaultValue: "123"}Value: &AttributeValue{DefaultValue: "123"}
DEFAULT TRUEValue: {$Type: "DomainModels$StoredValue", DefaultValue: "true"}Value: &AttributeValue{DefaultValue: "true"}
(calculated)Value: {$Type: "DomainModels$CalculatedValue", Microflow: <id>}Value: &AttributeValue{Type: "CalculatedValue", MicroflowID: id}

Default Value Syntax by Type

TypeDefault SyntaxExamples
StringDEFAULT 'value'DEFAULT '', DEFAULT 'Unknown'
IntegerDEFAULT nDEFAULT 0, DEFAULT -1
LongDEFAULT nDEFAULT 0
DecimalDEFAULT n.nDEFAULT 0, DEFAULT 0.00, DEFAULT 99.99
BooleanDEFAULT TRUE/FALSEDEFAULT TRUE, DEFAULT FALSE
AutoNumberDEFAULT nDEFAULT 1 (starting value)
EnumerationDEFAULT Module.Enum.ValueDEFAULT Shop.Status.Active, DEFAULT 'Pending'

Constraints

ConstraintSyntaxDescription
Not NullNOT NULLValue is required
Not Null with ErrorNOT NULL ERROR 'message'Required with custom error
UniqueUNIQUEValue must be unique
Unique with ErrorUNIQUE ERROR 'message'Unique with custom error

Constraints must appear in this order:

  1. NOT NULL [ERROR '...']
  2. UNIQUE [ERROR '...']
  3. DEFAULT <value>

Type Compatibility

MDL does not perform implicit type conversions. Types must match exactly.

Compatible type changes:

  • 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 not empty)

Complete Example

/** Example entity demonstrating all data types */
@Position(100, 100)
CREATE PERSISTENT ENTITY Demo.AllTypes (
  /** Auto-generated ID */
  Id: AutoNumber NOT NULL UNIQUE DEFAULT 1,

  /** Short text field */
  Code: String(10) NOT NULL UNIQUE,

  /** Standard text field */
  Name: String(200) NOT NULL,

  /** Long text field */
  Description: String(unlimited),

  /** Integer counter */
  Counter: Integer DEFAULT 0,

  /** Large number */
  BigNumber: Long,

  /** Money amount */
  Amount: Decimal DEFAULT 0.00,

  /** Flag field */
  IsActive: Boolean DEFAULT TRUE,

  /** Timestamp */
  CreatedAt: DateTime,

  /** Date only */
  BirthDate: Date,

  /** File attachment */
  Attachment: Binary,

  /** Status from enumeration */
  Status: Enumeration(Demo.Status) DEFAULT 'Active'
)
INDEX (Code)
INDEX (Name, CreatedAt DESC);

Reserved Words

Complete reference for MDL reserved words and quoting rules.

Quoting Rules

Most MDL keywords now work unquoted as entity names, attribute names, parameter names, and module names. Common words like Caption, Check, Content, Format, Index, Label, Range, Select, Source, Status, Text, Title, Type, Value, Item, Version, Production, etc. are all valid without quoting.

Only structural MDL keywords require quoting: Create, Delete, Begin, End, Return, Entity, Module.

Quoted Identifiers

Quoted identifiers escape any reserved word using double-quotes (ANSI SQL style) or backticks (MySQL style):

DESCRIBE ENTITY "ComboBox"."CategoryTreeVE";
SHOW ENTITIES IN "ComboBox";
CREATE PERSISTENT ENTITY Module.VATRate ("Create": DateTime, Rate: Decimal);

Both double-quote and backtick styles are supported. You can mix quoted and unquoted parts: "ComboBox".CategoryTreeVE.

Words That Do NOT Require Quoting

The following common words can be used as identifiers without quoting:

CategoryWords
UI termsCaption, Content, Label, Text, Title, Format, Style
Data termsIndex, Range, Select, Source, Status, Type, Value, Item, Version
Security termsProduction
OtherCheck, and most domain-specific terms

Words That Require Quoting

The following structural MDL keywords must be quoted when used as identifiers:

KeywordExample with quoting
Create"Create": DateTime
Delete"Delete": Boolean
Begin"Begin": DateTime
End"End": DateTime
Return"Return": String(200)
Entity"Entity": String(200)
Module"Module": String(200)

Boolean Attribute Defaults

Boolean attributes auto-default to false when no DEFAULT is specified. Mendix Studio Pro requires Boolean attributes to have a default value.

CALCULATED Attributes

CALCULATED marks an attribute as calculated (not stored). Use CALCULATED BY Module.Microflow to specify the calculation microflow. Calculated attributes derive their value from a microflow at runtime.

ButtonStyle Values

ButtonStyle supports all values: Primary, Default, Success, Danger, Warning, Info.

Mendix Version Compatibility

Supported Mendix Studio Pro versions and MPR format mapping.

Supported Versions

mxcli supports Mendix Studio Pro versions 8.x through 11.x.

Studio Pro VersionMPR FormatStatus
8.xv1Supported
9.xv1Supported
10.0 – 10.17v1Supported
10.18+v2Supported
11.xv2Supported (primary development target)

Note: Development and testing is primarily done against Mendix 11.6. Other versions are supported but may have untested edge cases.

MPR Format Versions

v1 (Mendix < 10.18)

  • Single .mpr SQLite database file
  • All documents stored as BSON blobs in the UnitContents table
  • Self-contained – one file holds the entire project

v2 (Mendix >= 10.18)

  • .mpr SQLite file for metadata only
  • mprcontents/ folder with individual .mxunit files for each document
  • Better suited for Git version control (smaller, per-document diffs)

The library auto-detects the format. No configuration is needed.

Widget Template Versions

Pluggable widget templates are versioned by Mendix release. The embedded templates cover:

Mendix VersionTemplate Set
10.6.0DataGrid2, ComboBox, Gallery, and others
11.6.0Updated templates for current widgets

When creating pages with pluggable widgets, the library selects the template matching the project’s Mendix version, falling back to the nearest earlier version if an exact match is not available.

Feature Availability by Version

FeatureMinimum VersionNotes
Domain models8.xFull support
Microflows8.x60+ activity types
Nanoflows8.xClient-side flows
Pages8.x50+ widget types
Pluggable widgets9.xRequires widget templates
Workflows10.xUser tasks, decisions, parallel splits
Business events10.xEvent service definitions
View entities10.xOQL-backed entities
MPR v2 format10.18Per-document file storage
Calculated attributes10.xCALCULATED BY microflow

MxBuild Compatibility

The mx validation tool must match the project’s Mendix version:

# Auto-download the correct MxBuild version
mxcli setup mxbuild -p app.mpr

# Check the project
~/.mxcli/mxbuild/*/modeler/mx check app.mpr

MxBuild is downloaded on demand and cached in ~/.mxcli/mxbuild/{version}/.

Platform Support

mxcli runs on:

PlatformArchitecture
Linuxamd64, arm64
macOSamd64, arm64 (Apple Silicon)
Windowsamd64, arm64

No CGO or C compiler is required – the binary is fully statically linked using pure Go dependencies.

Common Mistakes and Anti-Patterns

Frequent errors encountered when writing MDL scripts or using the SDK, with explanations of why they occur and how to avoid them.

EXTENDS Must Appear Before (

CRITICAL: The EXTENDS clause goes BEFORE the opening parenthesis, not after.

-- CORRECT: EXTENDS before (
CREATE PERSISTENT ENTITY Module.ProductPhoto EXTENDS System.Image (
  PhotoCaption: String(200)
);

-- WRONG: EXTENDS after ) = parse error!
CREATE PERSISTENT ENTITY Module.Photo (
  PhotoCaption: String(200)
) EXTENDS System.Image;

String Attributes Need Explicit Length

Always specify an explicit length for String attributes. While String defaults to 200, it is clearer and safer to use String(200) explicitly. Use String(unlimited) for long text.

-- GOOD: Explicit length
Name: String(200)
Description: String(unlimited)
Code: String(10)

-- AVOID: Implicit default length
Name: String

Never Create Empty List Variables as Loop Sources

If processing imported data, accept the list as a microflow parameter. DECLARE $Items List of ... = empty followed by LOOP $Item IN $Items is always wrong – the loop body will never execute.

-- WRONG: Loop over empty list does nothing
DECLARE $Items List of Module.Item = empty;
LOOP $Item IN $Items BEGIN
  -- This never executes!
END LOOP;

-- CORRECT: Accept the list as a parameter
CREATE MICROFLOW Module.ProcessItems
  PARAMETER $Items: List of Module.Item
BEGIN
  LOOP $Item IN $Items BEGIN
    -- Process each item
  END LOOP;
  RETURN true;
END;

Never Use Nested LOOPs for List Matching

Loop over the primary list and use RETRIEVE with a filter for O(N) lookup. Nested loops are O(N^2).

-- WRONG: O(N^2) nested loops
LOOP $Item IN $Items BEGIN
  LOOP $Match IN $Targets BEGIN
    IF $Item/Key = $Match/Key THEN
      -- ...
    END IF;
  END LOOP;
END LOOP;

-- CORRECT: O(N) with RETRIEVE
LOOP $Item IN $Items BEGIN
  RETRIEVE $Match FROM $Targets WHERE Key = $Item/Key LIMIT 1;
  IF $Match != empty THEN
    -- ...
  END IF;
END LOOP;

Association ParentPointer/ChildPointer Semantics Are Inverted

CRITICAL: Mendix BSON uses inverted naming for association pointers. This is counter-intuitive and a common source of bugs.

BSON FieldPoints ToMDL Keyword
ParentPointerFROM entity (FK owner)FROM Module.Child
ChildPointerTO entity (referenced)TO Module.Parent

For example, CREATE ASSOCIATION Mod.Child_Parent FROM Mod.Child TO Mod.Parent stores:

  • ParentPointer = Child.$ID (the FROM entity owns the foreign key)
  • ChildPointer = Parent.$ID (the TO entity is being referenced)

This affects entity access rules: MemberAccess entries for associations must only be added to the FROM entity (the one stored in ParentPointer). Adding them to the TO entity triggers CE0066 “Entity access is out of date”.

The same convention applies in domainmodel.Association: ParentID = FROM entity, ChildID = TO entity.

BSON Storage Names vs Qualified Names

Mendix uses different “storage names” in BSON $Type fields than the “qualified names” shown in the TypeScript SDK documentation. Using the wrong name causes TypeCacheUnknownTypeException when opening in Studio Pro.

Qualified Name (SDK/docs)Storage Name (BSON $Type)Note
CreateObjectActionCreateChangeAction
ChangeObjectActionChangeAction
DeleteObjectActionDeleteAction
CommitObjectsActionCommitAction
RollbackObjectActionRollbackAction
AggregateListActionAggregateAction
ListOperationActionListOperationsAction
ShowPageActionShowFormAction“Form” was original term for “Page”
ClosePageActionCloseFormAction“Form” was original term for “Page”

When adding new types, always verify the storage name by:

  1. Examining existing MPR files with the mx tool or SQLite browser
  2. Checking the reflection data in the reference/mendixmodellib/reflection-data/ directory
  3. Looking at the parser cases in sdk/mpr/parser_microflow.go

Mendix Expression String Escaping

When generating Mendix expression strings (e.g., in expressionToString()), single quotes within string literals must be escaped by doubling them: 'it''s here'. Do NOT use backslash escaping (\'). This matches Mendix Studio Pro’s expression syntax.

Microflows: Unsupported Constructs

These constructs will cause parse errors:

UnsupportedUse Instead
CASE ... WHEN ... END CASENested IF ... ELSE ... END IF
TRY ... CATCH ... END TRYON ERROR { ... } blocks on specific activities

Boolean Attributes Must Have Defaults

Mendix Studio Pro requires Boolean attributes to have a DEFAULT value. If no DEFAULT is specified, MDL auto-defaults to false.

-- GOOD: Explicit default
IsActive: Boolean DEFAULT true

-- RISKY: Implicit default to false
Deleted: Boolean

Always Validate Before Presenting to Users

Always run syntax and reference checks before delivering MDL scripts:

# Syntax + anti-pattern check
mxcli check script.mdl

# With reference validation against a project
mxcli check script.mdl -p app.mpr --references

Error Messages Reference

Common error messages from Studio Pro, mxcli, and the MDL parser, with explanations and solutions.

Studio Pro Errors

These errors appear when opening a project in Studio Pro after modification by mxcli.

TypeCacheUnknownTypeException

TypeCacheUnknownTypeException: The type cache does not contain a type
with qualified name DomainModels$Index

Cause: The BSON $Type field uses the qualifiedName instead of the storageName. These are often identical, but not always.

Solution: Check the metamodel reflection data for the correct storage name:

qualifiedName (wrong)storageName (correct)
DomainModels$EntityDomainModels$EntityImpl
DomainModels$IndexDomainModels$EntityIndex

Look up the type in reference/mendixmodellib/reflection-data/<version>-structures.json and use the storageName field value.

CE0463: Widget definition changed

CE0463: The widget definition of 'DataGrid2' has changed.

Cause: The widget’s WidgetObject properties do not match its PropertyTypes schema. This happens when:

  • A widget template is missing properties
  • Property values have incorrect types
  • The template was extracted from a different Mendix version

Solution:

  1. Create the same widget manually in Studio Pro
  2. Extract its BSON from the saved project
  3. Compare the template’s object section against the Studio Pro version
  4. Update sdk/widgets/templates/<version>/<Widget>.json to match

See the debug workflow in .claude/skills/debug-bson.md for step-by-step instructions.

CE0066: Entity access is out of date

CE0066: Entity access for 'MyModule.Customer' is out of date.

Cause: Association MemberAccess entries were added to the wrong entity. In Mendix, association access rules must only be on the FROM entity (the one stored in ParentPointer), not the TO entity.

Solution: Ensure MemberAccess entries for associations are added only to the entity that owns the foreign key (the FROM side of the association). Remove any association MemberAccess entries from the TO entity.

System.ArgumentNullException (ValidationRule)

System.ArgumentNullException: Value cannot be null.

Cause: A validation rule’s Attribute field uses a binary UUID instead of a qualified name string. The metamodel specifies BY_NAME_REFERENCE for this field.

Solution: Use a qualified name string (e.g., "Module.Entity.Attribute") for the Attribute field in ValidationRule BSON, not a binary UUID.

mxcli Parser Errors

Mismatched input

Line 3, Col 42: mismatched input ')' expecting ','

Cause: Syntax error in the MDL statement – typically a missing comma, semicolon, or unmatched bracket.

Solution: Check the MDL syntax at the reported line and column. Common issues:

  • Missing commas between attribute definitions
  • Missing semicolons at the end of statements
  • Unmatched parentheses or curly braces

No viable alternative

Line 1, Col 0: no viable alternative at input 'CREAT'

Cause: Unrecognized keyword or misspelling.

Solution: Check the keyword spelling. MDL keywords are case-insensitive but must be valid. Run mxcli syntax keywords for the full keyword list.

mxcli Execution Errors

Module not found

Error: module 'MyModule' not found in project

Cause: The referenced module does not exist in the .mpr file.

Solution: Check the module name with SHOW MODULES and verify the spelling. Module names are case-sensitive.

Entity not found

Error: entity 'MyModule.Customer' not found

Cause: The referenced entity does not exist in the specified module.

Solution: Check with SHOW ENTITIES IN MyModule. If the entity was just created, ensure the create statement executed successfully before referencing it.

Reference validation failed

Error: unresolved reference 'MyModule.NonExistent' at line 5

Cause: A qualified name references an element that does not exist in the project. This error appears with mxcli check script.mdl -p app.mpr --references.

Solution: Verify the referenced element exists, or create it before the referencing statement.

BSON Serialization Errors

Wrong array prefix

Symptom: Studio Pro fails to load the project or shows garbled data.

Cause: Missing or incorrect integer prefix in BSON arrays. Mendix BSON arrays require a count/type prefix as the first element:

{
  "Attributes": [3, { ... }, { ... }]
}

Solution: Ensure all arrays include the correct prefix value (typically 2 or 3). Check existing BSON output for the correct prefix for each array property.

Wrong reference format

Symptom: Studio Pro crashes or shows null reference errors.

Cause: Using BY_ID_REFERENCE (binary UUID) where BY_NAME_REFERENCE (qualified string) is expected, or vice versa.

Solution: Check the metamodel reflection data for the property’s kind field:

  • BY_ID_REFERENCE -> use binary UUID
  • BY_NAME_REFERENCE -> use qualified name string (e.g., "Module.Entity.Attribute")

Glossary

Mendix-specific terms and concepts for developers who are new to the Mendix platform.

A

Association A relationship between two entities. Analogous to a foreign key in relational databases. Mendix supports Reference (one-to-many or one-to-one) and ReferenceSet (many-to-many) types.

Attribute A property of an entity. Equivalent to a column in a relational database table. Types include String, Integer, Long, Decimal, Boolean, DateTime, AutoNumber, Binary, Enumeration, and HashedString.

Access Rule A security rule that controls which module roles can create, read, write, or delete instances of an entity, and which attributes they can access.

B

BSON Binary JSON. The serialization format Mendix uses to store model elements inside MPR files. Each document (entity, microflow, page, etc.) is stored as a BSON blob.

Building Block A reusable page fragment that can be dragged onto pages in Studio Pro. Similar to a Snippet but intended for the toolbox.

C

Catalog In mxcli, an in-memory SQLite database that indexes project metadata for fast querying, full-text search, and cross-reference navigation.

Constant A named value defined at the module level that can be configured per environment (development, acceptance, production). Used for API keys, URLs, feature flags, etc.

D

DataGrid A widget that displays a list of objects in a tabular format with columns, sorting, and search. The classic DataGrid uses server-side rendering; DataGrid2 is the pluggable widget version.

DataView A widget that displays a single object, providing a context for input widgets (TextBox, DatePicker, etc.) to read and write attributes.

Domain Model The data model for a module, consisting of entities, their attributes, and associations between them. Analogous to an entity-relationship diagram.

E

Entity A data type in the domain model. Equivalent to a table in a relational database. Entities can be persistent (stored in the database), non-persistent (in-memory only), or view (backed by an OQL query).

Enumeration A fixed set of named values (e.g., OrderStatus with values Draft, Active, Closed). Used as attribute types when the set of possible values is known at design time.

Event Handler Logic that runs automatically before or after a commit or delete operation on an entity. Configured on the entity in the domain model.

G

Gallery A pluggable widget that displays a list of objects in a card/tile layout, as opposed to the tabular layout of a DataGrid.

Generalization Entity inheritance. When entity B generalizes entity A, B inherits all of A’s attributes and associations. Analogous to class inheritance in object-oriented programming.

L

Layout A page template that defines the common structure (header, sidebar, footer) shared by multiple pages. Every page references a layout.

LayoutGrid A responsive grid widget based on a 12-column system. Used to create multi-column layouts with breakpoints for phone, tablet, and desktop.

M

MDL (Mendix Definition Language) A SQL-like text language for querying and modifying Mendix projects. The primary interface for mxcli.

Microflow Server-side logic expressed as a visual flow of activities. Microflows can retrieve data, call actions, make decisions, loop over lists, and return values. They run on the Mendix runtime (server).

Module A top-level organizational unit in a Mendix project. Each module contains its own domain model, microflows, pages, enumerations, and security settings. Analogous to a package or namespace.

Module Role A permission role defined within a module. Module roles are assigned to user roles at the project level. Entity access rules, microflow access, and page access are granted to module roles.

MPR Mendix Project Resource file. The binary file (.mpr) that contains the complete Mendix project model. It is a SQLite database with BSON-encoded documents.

N

Nanoflow Client-side logic that runs in the browser (or native app). Syntactically similar to microflows but with restrictions (no database transactions, limited activity types). Used for offline-capable and low-latency operations.

P

Page A user interface screen in a Mendix application. Pages contain widgets (TextBox, DataGrid, Button, etc.) and reference a layout for their outer structure.

Pluggable Widget A widget built with the Mendix Pluggable Widget API (React-based). Examples: DataGrid2, ComboBox, Gallery. Pluggable widgets use JSON-based property schemas (PropertyTypes) stored in the MPR.

R

Runtime The Mendix server-side execution environment (Java-based) that runs the application. It interprets the model, handles HTTP requests, executes microflows, and manages the database.

S

Scheduled Event A microflow that runs automatically on a timer (e.g., every hour, daily at midnight). Configured with a start time and interval.

Snippet A reusable UI fragment that can be embedded in multiple pages via a SnippetCall widget. Snippets can accept parameters.

Studio Pro The visual IDE for building Mendix applications. It reads and writes .mpr files. mxcli provides a complementary text-based interface to the same project files.

Storage Name The internal type identifier used in BSON $Type fields. Often the same as the qualified name, but not always (e.g., DomainModels$EntityImpl is the storage name for DomainModels$Entity).

U

User Role A project-level role that aggregates module roles from one or more modules. End users are assigned user roles, which determine their permissions across the entire application.

W

Widget A UI component on a page. Built-in widgets include TextBox, DataGrid, Button, Container, and LayoutGrid. Pluggable widgets extend this set with third-party components.

Workflow A long-running process with user tasks, decisions, and parallel paths. Workflows model approval processes, multi-step procedures, and human-in-the-loop automation.

TypeScript SDK Equivalence

Mapping between the official Mendix Model SDK (TypeScript) and MDL/modelsdk-go equivalents.

Overview

The Mendix Model SDK is Mendix’s official TypeScript library for programmatic model manipulation. It works through the Mendix Platform API and requires cloud connectivity. MDL and modelsdk-go provide similar capabilities but operate directly on local .mpr files.

Feature Comparison

FeatureMendix Model SDK (TypeScript)MDL / modelsdk-go
LanguageTypeScript / JavaScriptGo (library), MDL (DSL)
RuntimeNode.jsNative binary
Cloud requiredYes (Platform API)No (local files)
AuthenticationAPI key + PATNone
Real-time collaborationYesNo
Read operationsYesYes
Write operationsYesYes
Type safetyTypeScript typesGo types
CLI toolNoYes (mxcli)
SQL-like DSLNoYes (MDL)
AI assistant integrationLimitedBuilt-in (multi-tool)
Full-text searchNoYes (FTS5)
Cross-reference analysisNoYes (callers, impact)
LintingNoYes (Go + Starlark rules)

Operation Mapping

Reading

TypeScript SDKMDL EquivalentGo Library
model.allDomainModels()SHOW ENTITIESreader.ListDomainModels()
domainModel.entitiesSHOW ENTITIES IN Modulereader.GetDomainModel(id)
entity.attributesDESCRIBE ENTITY Module.Namedm.Entities[i].Attributes
model.allMicroflows()SHOW MICROFLOWSreader.ListMicroflows()
model.allPages()SHOW PAGESreader.ListPages()
model.allEnumerations()SHOW ENUMERATIONSreader.ListEnumerations()

Writing

TypeScript SDKMDL EquivalentGo Library
domainmodels.Entity.createIn(dm)CREATE PERSISTENT ENTITY Module.Name (...)writer.CreateEntity(dmID, entity)
entity.name = "Foo"Part of CREATE ENTITYentity.Name = "Foo"
domainmodels.Attribute.createIn(entity)Column in entity definitionwriter.AddAttribute(dmID, entityID, attr)
domainmodels.Association.createIn(dm)CREATE ASSOCIATIONwriter.CreateAssociation(dmID, assoc)
microflows.Microflow.createIn(folder)CREATE MICROFLOWwriter.CreateMicroflow(mf)
pages.Page.createIn(folder)CREATE PAGEwriter.CreatePage(page)
enumerations.Enumeration.createIn(module)CREATE ENUMERATIONwriter.CreateEnumeration(enum)

Security

TypeScript SDKMDL Equivalent
security.ModuleRole.createIn(module)CREATE MODULE ROLE Module.RoleName
Manual access rule constructionGRANT role ON Entity (permissions)
Manual user role creationCREATE USER ROLE Name (ModuleRoles)

Workflow Comparison

TypeScript SDK Workflow

const client = new MendixPlatformClient();
const app = await client.getApp("app-id");
const workingCopy = await app.createTemporaryWorkingCopy("main");
const model = await workingCopy.openModel();

const dm = model.allDomainModels().filter(d => d.containerAsModule.name === "Sales")[0];
await dm.load();

const entity = domainmodels.Entity.createIn(dm);
entity.name = "Customer";

const attr = domainmodels.Attribute.createIn(entity);
attr.name = "Name";
attr.type = domainmodels.StringAttributeType.create(model);

await model.flushChanges();
await workingCopy.commitToRepository("main");

MDL Equivalent

CREATE PERSISTENT ENTITY Sales.Customer (
    Name: String(200)
);

Go Library Equivalent

reader, _ := modelsdk.Open("app.mpr")
modules, _ := reader.ListModules()
dm, _ := reader.GetDomainModel(modules[0].ID)

writer, _ := modelsdk.OpenForWriting("app.mpr")
entity := modelsdk.NewEntity("Customer")
entity.Attributes = append(entity.Attributes,
    modelsdk.NewStringAttribute("Name", 200))
writer.CreateEntity(dm.ID, entity)

Key Differences

  1. No cloud dependency – modelsdk-go works entirely offline with local .mpr files
  2. No working copy management – changes are written directly to the file (back up first)
  3. MDL is declarative – one statement replaces multiple imperative SDK calls
  4. Token efficiency – MDL uses 5-10x fewer tokens than equivalent JSON model representations, making it better suited for AI assistant context windows
  5. Built-in search and analysis – full-text search, cross-reference tracking, and impact analysis are not available in the TypeScript SDK

Changelog

Version history and release notes for ModelSDK Go and the mxcli tool.

Releases

See GitHub Releases for the complete version history, including:

  • Release notes with feature descriptions
  • Pre-built binaries for all platforms (Linux, macOS, Windows – amd64 and arm64)
  • Source code archives

Versioning

mxcli follows Semantic Versioning:

  • Major – breaking changes to the MDL language or Go library API
  • Minor – new features and capabilities (backward-compatible)
  • Patch – bug fixes and documentation updates

Status

Alpha-quality software. This project is in early stages. Expect bugs, missing features, and rough edges. Always back up your Mendix project files before using mxcli to modify them.