generate-domain-model · git:20260821.ff81a24 · 2026-08-21 · sha256 2b7b18b3b6dad084
generate-domain-model git:20260821.ff81a24A
Immutable. This exact content is served forever at /api/v1/blob/2b7b18b3b6dad084.
---
name: generate-domain-model
description: "Generate a complete Mendix domain model in MDL — entities, attributes, associations, enumerations — and validate it. Use when asked for a domain model for a business area (e-commerce, HR, CRM, …) rather than a single entity."
---
# Creating Mendix Domain Model MDL Scripts
Use this skill to generate Mendix domain model scripts in MDL (Mendix Definition Language) format and validate them with the linter.
## When to Use This Skill
- User asks to create a domain model for a specific use case
- User wants to generate entities, associations, and enumerations
- User requests a complete e-commerce, HR, CRM, or other business domain model
- User needs validation of generated MDL scripts
## MDL Syntax Reference
**CRITICAL: All CREATE statements MUST have JavaDoc-style documentation**
Every CREATE statement (modules, entities, associations, enumerations, microflows) should have a /** ... */ comment explaining its purpose. This is essential for:
- Team collaboration and knowledge transfer
- Understanding domain model structure
- Long-term maintainability
- Auto-generated documentation
### Module Creation
```sql
/**
* Module for financial transaction management
*
* Handles accounts, transactions, budgets, and reporting.
*
* @since 1.0.0
*/
create module Finance;
```
### Minimap Section Headers (MARK Comments)
**IMPORTANT: Large MDL files (300+ lines) MUST use MARK comments for navigation**
Use `-- MARK: Section Name` comments to create collapsible sections in code editors. This dramatically improves navigation and organization in large domain model files.
**Format**: `-- MARK: Section Name`
**Required for files:**
- 300+ lines: At least 3 MARK comments
- 500+ lines: At least 5 MARK comments
**Recommended sections:**
```sql
-- MARK: ENUMERATIONS
-- MARK: CORE ENTITIES
-- MARK: ASSOCIATIONS
-- MARK: VIEW ENTITIES
-- MARK: MICROFLOWS
```
**With subsections:**
```sql
-- MARK: - Core Entities (Persistent)
-- MARK: - View Entities for Reporting
```
**Benefits:**
- Creates outline/minimap view in VS Code, Xcode-style editors
- Makes large files navigable with jump-to-section
- Groups related code logically
- Improves team collaboration on complex models
### Enumerations
```sql
/**
* Transaction type classification
*
* Categorizes financial transactions as income or expense
* for proper accounting and reporting.
*
* @since 1.0.0
*/
create enumeration Module.TransactionType (
INCOME 'Income',
EXPENSE 'Expense'
);
```
**Editing an existing enumeration** — use `alter enumeration`, never drop + recreate
(a drop is blocked while the enum is referenced by an attribute):
```sql
alter enumeration Module.TransactionType add value REFUND caption 'Refund';
alter enumeration Module.TransactionType rename value INCOME to CREDIT; -- changes the name/key
alter enumeration Module.TransactionType modify value EXPENSE caption 'Expense / Debit'; -- caption only, name unchanged
alter enumeration Module.TransactionType drop value REFUND;
```
`modify value … caption` re-captions in place — the value keeps its identity, so it
works even while the enumeration is in use. (Value names in `alter` must be plain
identifiers; a value whose name is a reserved word can't be targeted by `alter`.)
### Entities
**IMPORTANT: All entities MUST have @Position annotation**
The `@position(x, y)` annotation specifies where the entity appears in the domain model diagram. Without it, entities appear at (0,0) or random locations.
**Position Guidelines:**
- Use increments of 50 or 100 for spacing (e.g., 100, 200, 300)
- Leave space between entities (at least 200 pixels)
- Organize related entities in logical groups
- Example layout: Categories at y=100, Transactions at y=300, Reports at y=500
**Association line anchors** — where the connector attaches to each entity box —
are set with `@anchor`, as a **percentage of the box** (0..100, whole numbers):
```sql
@anchor(from: (0, 54), to: (100, 54))
create association Sales.Order_Customer
from Sales.Order to Sales.Customer;
```
`from` is the anchor on the FROM entity's box, `to` the anchor on the TO
entity's. `(0, 50)` is the middle of the left edge, `(100, 50)` the middle of the
right, `(50, 100)` the bottom centre.
Retune a line without restating the association:
```sql
alter association Sales.Order_Customer set anchor from (50, 100) to (50, 0);
```
**Naming an end sets it; not naming one preserves what is stored.** An
association written without `@anchor` keeps whatever the line was dragged to in
Studio Pro, so a `create or modify association` about the delete behaviour never
flattens someone's layout. `describe association` re-emits a non-default pair as
the same `@anchor(...)` annotation, so describe → edit → exec round-trips.
Cross-module associations have no anchors at all — Mendix stores none, and
`set anchor` on one is refused.
#### Persistent Entity
```sql
/**
* Entity description
*
* Detailed explanation of what this entity represents.
*
* @since 1.0.0
* @see Module.RelatedEntity
*/
@position(100, 100)
create persistent entity Module.EntityName (
/** Unique identifier */
Id: long not null error 'ID is required' unique error 'ID must be unique',
/** Attribute description */
attributename: string(200) not null error 'Attribute name is required',
/** Numeric value */
Amount: decimal,
/** Date field */
CreationDate: date,
/** Boolean flag */
IsActive: boolean not null error 'IsActive flag is required' default true,
/** Enumeration field */
status: enumeration(Module.StatusEnum) not null error 'Status is required'
);
```
#### Entity Indexes (Performance Optimization)
**CRITICAL: INDEX syntax goes AFTER the closing parenthesis, with NO comma before**
Indexes improve query performance for frequently filtered or sorted columns. Add them to persistent entities when:
- Column is used in WHERE clauses frequently
- Column is used for sorting (ORDER BY)
- Composite indexes for multi-column filters
**Syntax:**
```sql
create persistent entity Module.Transaction (
TransactionDate: datetime not null,
status: enumeration(Module.Status) not null,
Amount: decimal not null,
IsRecurring: boolean default false
)
index (TransactionDate desc)
index (status, TransactionDate)
index (IsRecurring);
```
**Index Guidelines:**
- **Position**: AFTER closing parenthesis, NO comma before first INDEX
- **No names**: Unlike SQL CREATE INDEX, MDL indexes don't have names
- **Sort direction**: ASC or DESC are optional (default is ASC)
- **Composite indexes**: Order matters - put most selective columns first
- **Limit**: Don't over-index - each index has storage/write overhead
**Common index patterns:**
- Date fields: `index (CreatedDate desc)` - for recent-first queries
- Status filters: `index (status, CreatedDate desc)` - for filtered date ranges
- Boolean flags: `index (IsActive)` - for active/inactive filtering
- Foreign keys: Automatically indexed by associations
#### Entity Generalization (EXTENDS)
**CRITICAL: EXTENDS goes BEFORE the opening parenthesis, not after!**
Use `extends` to inherit from a parent entity. Common for file/image storage using System entities.
```sql
-- Correct: EXTENDS before (
create persistent entity Module.ProductPhoto extends System.Image (
PhotoCaption: string(200),
SortOrder: integer default 0
);
-- Correct: File document specialization
create persistent entity Module.Attachment extends System.FileDocument (
AttachmentDescription: string(500)
);
-- Correct: Custom entity inheritance
create persistent entity Module.Employee extends Module.Person (
EmployeeNumber: string(20)
);
```
**Wrong** (parse error):
```sql
-- EXTENDS after ) = parse error!
create persistent entity Module.Photo (
PhotoCaption: string(200)
) extends System.Image;
```
**Note:** `mxcli syntax entity` output may show EXTENDS after `)` — this is misleading. Always place EXTENDS before `(`.
**Security follows inheritance.** Mendix inheritance is multi-table: all of the
parent's attributes are members of the child, so a specialized entity's access rule
must cover them. Grant an inherited member exactly like one of the entity's own —
`grant Module.Viewer on Module.Attachment (read (AttachmentDescription, "Name", Size));`
— and `read *` / `write *` cover them too. Skipping them is Mendix CE0066 "Entity
access is out of date". The one exception is entities extending `System.User`, whose
inherited platform members Mendix manages and which must not be granted. See
`manage-security`.
#### System Attributes (Auditing)
Mendix supports four built-in auditing properties on persistent entities. Declare them as regular attributes using pseudo-types (like `autonumber`):
| Pseudo-Type | System Attribute | Set When |
|-------------|-----------------|----------|
| `autoowner` | `System.owner` (→ System.User) | Object created |
| `autochangedby` | `System.changedBy` (→ System.User) | Every commit |
| `autocreateddate` | `CreatedDate` (DateTime) | Object created |
| `autochangeddate` | `ChangedDate` (DateTime) | Every commit |
```sql
/**
* Order with full audit trail
*/
create persistent entity Sales.Order (
OrderNumber: autonumber default 1,
TotalAmount: decimal not null,
status: enumeration(Sales.OrderStatus) not null,
owner: autoowner,
ChangedBy: autochangedby,
CreatedDate: autocreateddate,
ChangedDate: autochangeddate
);
```
To enable/disable on existing entities, use ALTER ENTITY ADD/DROP ATTRIBUTE:
```sql
alter entity Sales.Order add attribute owner: autoowner;
alter entity Sales.Order add attribute ChangedDate: autochangeddate;
alter entity Sales.Order drop attribute ChangedBy;
```
**When to use auditing:**
- Compliance/regulated domains (finance, healthcare) — use all four
- User-generated content — use AutoOwner for ownership-based access rules
- "Recently modified" lists — use AutoChangedDate
- Avoid on high-volume system tables (every write touches the audit columns)
#### Non-Persistent Entity
**IMPORTANT: Non-persistent entities cannot have validation rules** (`not null error`, `unique error`) on attributes. They can only have `default` values.
```sql
/**
* Non-persistent entity description
*
* @since 1.0.0
*/
@position(200, 100)
create non-persistent entity Module.TemporaryData (
SessionId: string(100),
data: string(1000),
IsActive: boolean default false
);
```
#### View Entity (with OQL)
```sql
/**
* View entity description
*
* @since 1.0.0
*/
@position(300, 500)
create view entity Module.ViewName (
Attribute1: type,
Attribute2: type
) as (
select
e.Id as Id,
e.Name as Name,
e.Amount as Amount
from Module.Entity as e
where e.IsActive = true
);
```
**Enumeration Comparisons in OQL:**
When comparing enumeration attributes in OQL WHERE clauses, use the **enumeration value** (identifier), not the caption:
```sql
-- Enumeration definition
create enumeration Module.OrderStatus (
PENDING 'Pending',
PROCESSING 'Processing',
CANCELLED 'Cancelled'
);
-- OQL comparison - use the VALUE, not the caption
where e.Status != 'CANCELLED' -- Correct: uses enum value
where e.Status != 'Cancelled' -- Wrong: this is the caption
```
### Entity Event Handlers
Microflows can run before/after entity Create, Commit, Delete, or Rollback. Use the optional `raise error` clause to make a handler act as a validation microflow — if it returns false, the operation is aborted.
```sql
-- In CREATE ENTITY (handlers go after attributes/indexes)
create persistent entity Sales.Order (
Total: decimal,
status: string(50)
)
on before commit call Sales.ACT_ValidateOrder raise error
on after create call Sales.ACT_InitDefaults;
-- Add via ALTER ENTITY
alter entity Sales.Order
add event handler on before delete call Sales.ACT_CheckCanDelete raise error;
-- Drop via ALTER ENTITY
alter entity Sales.Order
drop event handler on before commit;
```
**Moments**: `before`, `after`
**Events**: `create`, `commit`, `delete`, `rollback`
Each (Moment, Event) combination can only have one handler per entity. The microflow must exist (the executor validates the reference). `raise error` is optional — without it, the handler runs but its return value doesn't affect the operation.
### Associations
**CRITICAL: Association Directionality**
In Mendix, associations are defined **FROM the entity that contains the foreign key TO the entity that is referenced**.
Think of it like this:
- A `Transaction` knows which `Account` it belongs to → Transaction contains the foreign key
- Therefore: `from Transaction to Account`
- **NOT** `from Account to Transaction` ❌
**Common Patterns**:
```sql
-- ❌ INCORRECT: Account doesn't store transaction references
create association Finance.Account_Transaction
from Finance.Account to Finance.Transaction
type reference;
-- ✅ CORRECT: Transaction stores the account reference (foreign key)
create association Finance.Transaction_Account
from Finance.Transaction to Finance.Account
type reference;
-- ✅ One-to-Many: Customer has many Orders (each order knows its customer)
create association Sales.Order_Customer
from Sales.Order to Sales.Customer
type reference;
-- ✅ Many-to-Many: Use ReferenceSet and choose which side stores the relationship
create association Sales.Order_Products
from Sales.Order to Sales.Product
type ReferenceSet
owner both;
```
**Full Association Syntax**:
```sql
/**
* Association description
*
* Explain the relationship and directionality.
*
* @since 1.0.0
*/
create association Module.EntityWithFK_ReferencedEntity
from Module.EntityWithFK to Module.ReferencedEntity
type reference
owner default
delete_behavior DELETE_BUT_KEEP_REFERENCES
comment 'Additional documentation';
```
**Idempotency**: plain `create association` is **not** idempotent — re-running it
errors with `association already exists`, which aborts the rest of the script (and
any associations defined *after* it are never created). Write **`create or modify
association`** from the first draft — same clauses, but re-running is a no-op:
```sql
create or modify association Module.Child_Parent
from Module.Child to Module.Parent
type reference;
```
**Association Types**:
- `reference` - One-to-one or many-to-one (foreign key on FROM entity)
- `ReferenceSet` - One-to-many or many-to-many (collection)
**Owner Options**:
- `default` - Standard ownership (FROM entity owns the reference)
- `both` - Both sides own the association (bidirectional)
- `Parent` - Only parent (TO) entity owns
- `Child` - Only child (FROM) entity owns
> **Use `default` ownership for a normal to-one reference.** Reserve `owner both`
> for a `ReferenceSet` (many-to-many). On a plain `type reference`, `owner both`
> makes the association navigable **to-one from *both* sides** — so the reverse
> direction is a single object, not a collection. A **list** widget (listview/
> datagrid/gallery) over the reverse then fails MxBuild **CE8812** "A grid
> association path must result in a list." (a DataView over the reverse is fine —
> it's a single object). With `default` ownership, the reverse is the expected
> to-many collection and a list widget works. See master-detail-pages for the
> widget patterns.
**Delete Behaviors**:
- `DELETE_AND_REFERENCES` - Delete object and all referencing objects
- `DELETE_BUT_KEEP_REFERENCES` - Delete object, keep references (nullify)
- `DELETE_IF_NO_REFERENCES` - Only delete if no objects reference it
- `cascade` - Cascade delete to associated objects
- `prevent` - Prevent deletion if references exist
**Naming Convention**: `{FromEntity}_{ToEntity}` (e.g., `Order_Customer`, `Transaction_Account`)
#### Calculated Attributes
Calculated attributes derive their value from a microflow at runtime. Use `calculated by Module.Microflow` to specify the calculation microflow.
**IMPORTANT: CALCULATED attributes are only supported on PERSISTENT entities.** Using CALCULATED on non-persistent entities will produce a validation error.
```sql
@position(100, 100)
create persistent entity Module.OrderLine (
/** Unit price */
UnitPrice: decimal not null,
/** Quantity ordered */
Quantity: integer not null,
/** Total price, calculated by microflow */
TotalPrice: decimal calculated by Module.CalcTotalPrice
);
```
**Syntax variants:**
- `calculated by Module.Microflow` — recommended, binds the calculation microflow directly
- `calculated Module.Microflow` — also valid (`by` keyword is optional)
- `calculated` — bare form, marks as calculated but requires manual microflow binding in Studio Pro
**The microflow's signature is checked, and mxcli refuses a mismatch before
writing** — Mendix reports these as **CE7247** at build time (verified on 11.13.0):
| Microflow | Result |
|-----------|--------|
| takes the owning entity (`$Order: Module.Order`) | ✅ stored with `PassEntity = true` |
| takes **no** parameter | ✅ stored with `PassEntity = false` — equally valid |
| takes a *different* entity | ❌ refused: CE7247 *"Microflow parameter 'X' should be of type Module.Order."* |
| takes two or more parameters | ❌ refused |
| returns the wrong type | ❌ refused: CE7247 *"Microflow return type should be …"* |
| returns `Long` for an `integer` attribute (or vice versa) | ✅ accepted — Integer and Long are one family here |
A microflow **created earlier in the same script** cannot be inspected yet, so
its signature is not checked; the build has the last word on those.
> **Before mxcli 0.17 the binding was silently discarded** on the default
> engine: the attribute was written as an ordinary stored value, `mx check`
> reported 0 errors, and the attribute stayed empty at runtime (#917). If you
> have attributes that were declared `calculated by` and never calculated, they
> need re-running through a current mxcli — re-executing the same statement is
> enough.
### Data Types
| Type | Example | Description |
|------|---------|-------------|
| `string(length)` | `string(200)` | Text field with max length |
| `integer` | `integer` | 32-bit integer |
| `long` | `long` | 64-bit integer (use for IDs) |
| `decimal` | `decimal` | Decimal number |
| `boolean` | `boolean` | True/false |
| `datetime` | `datetime` | Date and time |
| `date` | `date` | Date only |
| `binary` | `binary` | Binary data |
| `autonumber` | `autonumber default 1` | Auto-incrementing number (requires DEFAULT start value) |
| `enumeration(Module.Enum)` | `enumeration(Shop.Status)` | Enumeration reference |
### Constraints
**Basic Constraints:**
- `not null` - Field is required
- `unique` - Value must be unique
- `default value` - Default value
**Validation Error Messages:**
Each constraint can have a custom error message using `error 'message'` syntax:
```sql
create persistent entity Module.Customer (
/** Customer name - required with custom error */
Name: string(200) not null error 'Name is required',
/** Email - required and unique with separate error messages */
Email: string(200) not null error 'Email is required' unique error 'Email must be unique',
/** Age with default value */
Age: integer default 0,
/** Active status flag */
IsActive: boolean not null error 'IsActive flag is required' default true
);
```
**Error Message Guidelines:**
- Place `error 'message'` immediately after the constraint
- Multiple constraints can each have their own error message
- Keep messages clear and user-friendly
- Follow the pattern: `not null error 'X is required'` for required fields
- For UNIQUE: `unique error 'X must be unique'`
- Error messages are shown to end users during validation
**Common patterns:**
```sql
-- Required field
Name: string(200) not null error 'Name is required',
-- Required and unique
Email: string(200) not null error 'Email is required' unique error 'Email must be unique',
-- Required with default
IsActive: boolean not null error 'IsActive flag is required' default true,
-- Enum with required error
status: enumeration(Module.Status) not null error 'Status is required',
-- Enum with default value (use fully qualified Module.Enum.Value)
Priority: enumeration(Module.Priority) default Module.Priority.Normal
```
## Reserved Keywords
**Best practice: Always quote all identifiers** (entity names, attribute names) with double quotes. This escapes every **MDL parser** keyword conflict — quotes are stripped automatically by the parser. So `"create"`, `"status"`, `"end"` become valid attribute names.
```sql
create persistent entity Module."VATRate" (
"create": datetime,
"Rate": decimal,
"status": string(50)
);
```
> **Caveat — quoting does not exempt *platform*-reserved member names.** Some names are
> reserved by the Mendix *platform*, not just the MDL parser, and are rejected **even when
> quoted** (the check strips the quotes and still flags them): `Type` (CE7247, MDL021),
> the audit attributes `CreatedDate` / `ChangedDate` / `Owner` / `ChangedBy` (MDL020 — use
> the `AutoCreatedDate` / `AutoChangedDate` / `AutoOwner` / `AutoChangedBy` pseudo-types
> instead), plus `ID`, `GUID`, `CurrentUser` and the Java-keyword list. `"Type": String`
> fails MDL021 — rename to `ResourceType` / `TypeValue`.
Both `"Name"` and `` `Name` `` syntax are supported. Prefer double quotes for consistency.
**Boolean attributes** auto-default to `false` when no `default` is specified:
```sql
create persistent entity Module.Item (
IsActive: boolean, -- auto-defaults to false
IsPublished: boolean default true
);
```
## Entity Positioning
Use `@position(x, y)` to control layout in Studio Pro:
- Place related entities near each other
- Use consistent spacing (e.g., 250 pixels horizontal, 200 vertical)
- Group by domain concept
Example layout:
```sql
@position(50, 50) -- Top-left: Core entity
create persistent entity Module.Customer (...);
@position(300, 50) -- Same row: Related entity
create persistent entity Module.Address (...);
@position(50, 250) -- Below: Dependent entity
create persistent entity Module.Order (...);
```
## Modelling for the client: widgets bind members, not expressions
A page widget binds an **attribute or association path** — never an expression. There
is no `substring(attr, i, 1)` or computed binding in a widget. So a value that must be
rendered per-part (each character of a code, each cell of a grid, each of N pencil
marks) has to be stored as **separate attributes**, not packed into one string and
indexed client-side. Model the wide form when the UI needs to address the parts
individually (e.g. `N1`…`N9` booleans rather than a packed `Notes` string); reach for
a computed/derived value only where a microflow or view-entity OQL produces it into a
real attribute. (Same reason the bucket-class idiom exists — see
`migrate-design-prototype`.)
## Documentation Best Practices
### Entity Documentation
```sql
/**
* Brief one-line summary
*
* Detailed multi-line description explaining:
* - What the entity represents
* - Key business rules
* - Relationships to other entities
*
* @since 1.0.0
* @see Module.RelatedEntity
*/
```
### Attribute Documentation
```sql
/** Brief description of what this attribute stores */
attributename: type,
```
### Association Documentation
```sql
/**
* Relationship description
*
* Explains the business meaning of this association.
*
* @since 1.0.0
*/
```
## Step-by-Step Process
### 1. Analyze Requirements
When user requests a domain model:
1. Identify core entities (nouns)
2. Identify enumerations (status, types, categories)
3. Identify relationships (associations)
4. Identify attributes for each entity
5. Check for reserved keyword conflicts
### 2. Generate MDL Script
Create script with this structure:
```sql
-- ============================================================================
-- Domain Model Name
-- ============================================================================
-- Description of the domain
-- ============================================================================
-- MARK: ENUMERATIONS
create enumeration Module.Enum1 (...);
create enumeration Module.Enum2 (...);
-- MARK: CORE ENTITIES
-- MARK: - Entity Group 1
create persistent entity Module.Entity1 (...);
create persistent entity Module.Entity2 (...);
-- MARK: - Entity Group 2
create persistent entity Module.Entity3 (...);
-- MARK: VIEW ENTITIES
create view entity Module.View1 as ...;
-- MARK: ASSOCIATIONS
-- MARK: - Entity Group 1 Associations
create association Module.Assoc1 ...;
create association Module.Assoc2 ...;
```
### 3. Validate with Linter
Run the linter to check for issues:
```bash
# Standalone test
node dist/test-linter-standalone.js
# or create a custom test file
```
The linter will detect:
- ✅ Reserved keywords (CE7247)
- ✅ Duplicate names (CE0065)
- ✅ OQL syntax errors (CE0174)
### 4. Review and Fix Issues
**Common Issues**:
1. **Reserved Keyword Error**:
```
error: Reserved keyword 'CreatedDate' used as attribute name
💡 rename to 'CreationDate'
```
Fix: Rename to suggested alternative
2. **Duplicate Name Error**:
```
error: Duplicate name 'Status' in module 'Shop'
💡 rename one of the enumeration, entity to avoid conflict
```
Fix: Rename entity to `OrderStatus` or similar
3. **OQL Syntax Error**:
```
error: ORDER by requires limit or offset
💡 add limit clause to query
```
Fix: Add `limit 100` to view entity query
### 5. Generate Complete Script
Ensure:
- ✅ All entities have JavaDoc documentation
- ✅ All attributes have inline comments
- ✅ All associations have descriptions
- ✅ Position annotations for all entities
- ✅ No reserved keywords
- ✅ No duplicate names
- ✅ Valid OQL queries
## Example: E-Commerce Domain Model
```sql
-- ============================================================================
-- E-Commerce Domain Model
-- ============================================================================
create module ECommerce;
-- Enumerations
-- ============================================================================
/**
* Order status enumeration
*
* @since 1.0.0
*/
create enumeration ECommerce.OrderStatus (
Draft 'Draft',
Submitted 'Submitted',
Paid 'Paid',
Shipped 'Shipped',
Delivered 'Delivered',
Cancelled 'Cancelled'
);
-- Entities
-- ============================================================================
-- Customer Management
-- ----------------------------------------------------------------------------
/**
* Customer entity
*
* Stores customer information for e-commerce platform.
*
* @since 1.0.0
* @see ECommerce.SalesOrder
*/
@position(50, 50)
create persistent entity ECommerce.Customer (
/** Unique customer identifier */
CustomerId: long not null error 'Customer ID is required' unique error 'Customer ID must be unique',
/** Customer full name */
FullName: string(200) not null error 'Full name is required',
/** Email address */
Email: string(200) not null error 'Email is required' unique error 'Email must be unique',
/** Registration date */
RegistrationDate: datetime not null error 'Registration date is required'
);
/**
* Product entity
*
* Catalog of products available for purchase.
*
* @since 1.0.0
*/
@position(50, 250)
create persistent entity ECommerce.Product (
/** Unique product identifier */
ProductId: long not null error 'Product ID is required' unique error 'Product ID must be unique',
/** Product name */
ProductName: string(200) not null error 'Product name is required',
/** Product SKU */
SKU: string(50) not null error 'SKU is required' unique error 'SKU must be unique',
/** Unit price */
Price: decimal not null error 'Price is required',
/** Stock quantity */
StockQuantity: integer not null error 'Stock quantity is required'
);
/**
* Sales order entity
*
* Customer orders for products.
*
* @since 1.0.0
*/
@position(300, 150)
create persistent entity ECommerce.SalesOrder (
/** Unique order identifier */
OrderId: long not null error 'Order ID is required' unique error 'Order ID must be unique',
/** Order number */
OrderNumber: string(50) not null error 'Order number is required' unique error 'Order number must be unique',
/** Order date */
OrderDate: datetime not null error 'Order date is required',
/** Total amount */
TotalAmount: decimal not null error 'Total amount is required',
/** Order status */
status: enumeration(ECommerce.OrderStatus) not null error 'Status is required'
);
-- Associations
-- ============================================================================
/**
* Customer orders
*
* Links customers to their orders.
*
* @since 1.0.0
*/
create association ECommerce.Customer_Orders
from ECommerce.Customer to ECommerce.SalesOrder
type ReferenceSet
owner both;
```
## Testing the Script
1. **Save to file**: Save as `examples/my-domain-model.mdl`
2. **Run standalone linter**:
```bash
node dist/test-linter-standalone.js
```
3. **Execute in REPL**:
```sql
mendix> connect to FILESYSTEM 'path/to/project.mpr';
mendix> execute script 'examples/my-domain-model.mdl';
```
4. **Check Studio Pro**: Open project and verify entities appear correctly
## Common Patterns
### One-to-Many Relationship
```sql
-- Parent entity
create persistent entity Module.Parent (Id: long not null unique);
-- Child entity
create persistent entity Module.Child (
Id: long not null unique,
ChildData: string(200)
);
-- Association (Parent has many Children)
create association Module.Parent_Children
from Module.Parent to Module.Child
type ReferenceSet
owner both;
```
### Many-to-Many Relationship
```sql
-- Entity A
create persistent entity Module.EntityA (Id: long not null unique);
-- Entity B
create persistent entity Module.EntityB (Id: long not null unique);
-- Bidirectional association
create association Module.EntityA_EntityB
from Module.EntityA to Module.EntityB
type ReferenceSet
owner both;
```
### Hierarchical Relationship (Self-Reference)
**IMPORTANT: Self-referencing associations must use `owner default`** (one-to-many). Using `owner both` is not supported for self-references.
```sql
/**
* Category with parent-child hierarchy
*/
create persistent entity Module.Category (
Id: long not null unique,
CategoryName: string(200) not null
);
/**
* Parent category link (self-reference)
*/
create association Module.Category_ParentCategory
from Module.Category to Module.Category
type reference
owner default;
```
### ALTER ENTITY (Incremental Modifications)
Use `alter entity` to make targeted changes to existing entities without redefining the entire entity:
```sql
-- Add a new attribute
alter entity Module.Customer
add attribute PhoneNumber: string(20);
-- Add multiple attributes at once
alter entity Module.Order
add attribute VATRate: decimal
add attribute VATAmount: decimal;
-- Rename an attribute (preserves data). Every stored reference follows it:
-- microflow create/change members, page attribute widgets, validation rules,
-- access rules -- and XPath constraints too ([CreatedDate > ...]), including
-- ones that reach the entity through an association. Microflow expressions
-- ($Order/CreatedDate) are NOT rewritten -- mxbuild reports those as CE0117,
-- so build afterwards.
alter entity Module.Order
rename attribute CreatedDate to OrderDate;
-- Drop an attribute
alter entity Module.Product
drop attribute LegacyCode;
-- Modify attribute type
alter entity Module.Customer
modify attribute Address: string(500);
-- Modify attribute constraints. MODIFY applies the constraints you specify and
-- preserves the ones you don't:
-- NULLABLE -> make a required attribute optional (removes NOT NULL)
-- NOT NULL -> make an optional attribute required
-- UNIQUE -> add a uniqueness constraint
-- DEFAULT x -> set/replace the default
alter entity Module.Customer
modify attribute Email: string(200) nullable; -- Email is now optional
alter entity Module.Customer
modify attribute Code: string(20) not null unique;
-- Set entity documentation
alter entity Module.Customer
set documentation 'Core customer entity for CRM module';
-- Add an index
alter entity Module.Customer
add index idx_email (Email asc);
-- Reposition entity on domain model canvas
alter entity Module.Customer
set position (100, 200);
```
**Supported operations:** ADD ATTRIBUTE, RENAME ATTRIBUTE, MODIFY ATTRIBUTE (type + `NULLABLE`/`NOT NULL`/`UNIQUE`/`DEFAULT` constraints), DROP ATTRIBUTE, SET DOCUMENTATION, SET COMMENT, ADD INDEX, DROP INDEX, SET POSITION.
> **`MODIFY ATTRIBUTE` always takes a type** — restate it even when you only want
> to change a constraint. Its type slot accepts a bare qualified name, so a
> clause written in the type position is read as a type name:
> `MODIFY ATTRIBUTE X SET DEFAULT 0` treats `SET` as the type. mxcli now refuses
> that; before it did, the statement rewrote the attribute to an enumeration and
> produced a project Mendix could not open (#910).
>
> To clear a default value use **`DROP DEFAULT ON ATTRIBUTE <name>`**.
### Entity Positioning Guidelines
When creating or repositioning entities, follow these layout rules for readable domain models:
- **Horizontal spacing:** 350px between columns (x = 50, 400, 750, 1100, ...)
- **Vertical spacing:** calculate per-column based on the entity above: `y = previous_y + 50 + (previous_entity_attribute_count * 20)`
- Entity header is ~40px, each attribute adds ~20px of height, plus ~50px padding
- **Position column-by-column**, not in rigid rows — avoids wasting space when entities have different attribute counts
- **Place related entities** in the same column or adjacent columns so associations are short
Example layout for entities with varying attribute counts:
```
column 1 (x=50): column 2 (x=400):
entity A (4 attrs) entity C (14 attrs)
y=50 y=50
entity B (10 attrs) entity D (3 attrs)
y=180 (50+50+4*20) y=380 (50+50+14*20)
```
```sql
-- Position entities after creation
alter entity Module.EntityA set position (50, 50);
alter entity Module.EntityB set position (50, 180);
alter entity Module.EntityC set position (400, 50);
alter entity Module.EntityD set position (400, 380);
```
### Entity Migration with CREATE OR MODIFY
Use `create or modify` to update existing entities without losing data. The REPL computes differences and applies incremental changes.
```sql
/**
* Customer entity migration - rename CustomerName to FullName
*/
create or modify persistent entity Module.Customer (
/** Unique identifier (unchanged) */
CustomerId: long not null unique,
/** Renamed from CustomerName - data preserved */
@RenamedFrom('CustomerName')
FullName: string(200) not null,
/** New field */
Email: string(255) unique,
/** Type widened from String(100) to String(200) */
Address: string(200)
);
```
**Key features:**
- `@RenamedFrom('oldName')` - renames attribute, preserves data
- Auto-removes attributes not in new definition
- Allows compatible type changes (e.g., String length increase)
- Preserves entity UUID (no data loss)
### Status-Driven Entity
```sql
-- Status enumeration
create enumeration Module.TaskStatus (
Todo 'To Do',
InProgress 'In Progress',
Done 'Done'
);
-- Entity with status
create persistent entity Module.Task (
Id: long not null unique,
TaskName: string(200) not null,
status: enumeration(Module.TaskStatus) not null
);
```
## Checklist
Before finalizing an MDL script:
- [ ] All entities have JavaDoc documentation
- [ ] All attributes have inline comments
- [ ] All associations have descriptions
- [ ] Position annotations on all entities
- [ ] MARK comments for files 300+ lines (at least 3 sections)
- [ ] All identifiers quoted with double quotes
- [ ] No duplicate names (run linter)
- [ ] Valid OQL queries in view entities (run linter)
- [ ] Consistent naming conventions (PascalCase)
- [ ] Appropriate data types and lengths
- [ ] Required fields marked with NOT NULL
- [ ] Validation error messages added for NOT NULL and UNIQUE constraints
- [ ] IDs marked with NOT NULL UNIQUE
- [ ] Email/unique fields marked with UNIQUE
## References
- **Reserved Keywords**: `packages/mendix-repl/docs/reference/reserved-keywords.md`
- **Linter Proposal**: `packages/mendix-repl/docs/proposals/mdl-linter-proposal.md`
- **Example Scripts**:
- `packages/mendix-repl/examples/shop-domain-model.mdl`
- `packages/mendix-repl/examples/pet-store-domain-model.mdl`
- **Linter Test**: `packages/mendix-repl/src/test-linter-standalone.ts`
## Tips for AI Assistants
1. **Always quote all identifiers** with double quotes to avoid MDL parser keyword conflicts — but note quoting does **not** exempt platform-reserved member names (`Type`, `CreatedDate`, `ChangedDate`, `Owner`, `ChangedBy`, `ID`, …); rename those
2. **Use descriptive names** (ServiceType, CustomerOrder)
3. **Run linter** on generated scripts before presenting to user
4. **Fix all errors** reported by linter before finalizing
5. **Follow examples** in shop-domain-model.mdl and pet-store-domain-model.mdl
6. **Document thoroughly** - Studio Pro users benefit from good documentation
7. **Position thoughtfully** - Related entities should be visually grouped
8. **Test incrementally** - Generate in sections and validate each part