Security Management Skill
SkillSecurityLets your agent configure Mendix app security: user roles, access rules, project security level, demo users and guest access.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Security Management Skill skill
About this capability
Configure Mendix security in MDL, module and user roles, access to microflows, pages and entities, project security level, demo users, and guest access. Use when setting up or changing who can see or do what in an app.
What this skill tells your AI
The instructions your AI receives, as published by mendixlabs/mxcli in .claude/skills/mendix/manage-security/SKILL.md and read by ahel’s review.
This skill covers Mendix security configuration via MDL: module roles, user roles, access control (microflows, pages, entities), project security settings, and demo users.
When to Use This Skill
Use when the user asks to:
- Set up security for a module or project
- Create or manage module roles / user roles
- Grant or revoke access to microflows, pages, or entities
- Configure project security level or demo users
- Review existing security configuration
Security Concepts
- Module Roles define permissions within a single module (e.g.,
Shop.Admin,Shop.Viewer) - User Roles aggregate module roles from multiple modules (e.g.,
AdministratorincludesShop.Admin+System.Administrator) - Access Rules control CRUD rights on entities per module role
- Microflow/Page Access controls which module roles can execute/view specific elements
- Project Security Level determines enforcement:
off,prototype, orproduction
The System-module ceiling — decide the role model around this FIRST
No project module can widen access to System.User, System.Workflow or
System.WorkflowUserTask. Access to System entities comes from the System
module's own roles, and a grant in your module cannot raise it. This is a design
constraint, not a detail: it decides what your screens can be, and finding it late
means rebuilding them (ako/mxcli-maintenance-2 designed a technician picker, built
it, tested it, and tore it out).
Every consequence is silent — the page renders, the data is simply missing, and
mx check, mxcli lint and mxcli report all pass:
| What you build | What a non-Administrator sees |
|---|---|
A combo box over System.User (e.g. "pick a technician") | The current user only |
A grid over System.Workflow / System.WorkflowUserTask | Empty |
| Either of those, re-sourced from a microflow | Every row present, every field blank |
The rule: a microflow data source moves the ROWS, not the MEMBERS
Learn this as a rule rather than as a symptom, because the obvious workaround only looks like it worked, and the same trap is waiting on the next screen.
A microflow does not apply entity access, so its retrieve returns every row. But the runtime re-applies entity access when it serializes those objects to the client, XPath constraint included — so a row the role may not read arrives with every member empty. The list comes out the right length and the cards come out blank.
Measured in a browser on Mendix 11.14.0 — one page, two microflow-sourced lists over
the same System.User retrieve, opened by two users:
| list | as Administrator | as a plain User |
|---|---|---|
A — the System.User objects themselves | probe_admin, probe_viewer | (blank), probe_viewer |
B — a module-owned copy, Name read inside the microflow | probe_admin, probe_viewer | probe_admin, probe_viewer |
Both lists hold two rows for both users, so the microflow really did carry the rows
past entity access. Only A loses the values, and it loses them per object:
System.User's own rule grants read where [id = '[%CurrentUser%]'], which is why
a user picker shows you yourself and nobody else rather than showing nothing.
System.WorkflowUserTask has no such escape hatch for an ordinary role, so a
workflow inbox built this way comes out entirely blank — the reported case
(ako/mxcli#587): the inbox drew the right number of cards, every one of them empty,
with mxcli check, lint, report and docker check all at 0 errors.
What to do instead
- Record, don't pick. Have the workflow stamp itself against a row your own
module owns — an
ON CREATED MICROFLOWwriting an association plus a plain status string — and list those objects. Target a task at a role, let whoever opens it do the work, and record who acted when they act. - Read it inside a microflow, return your OWN object. Entity access does not
apply to the retrieve or to the member read, only to what crosses to the client
— so copy the values you need onto an entity your module owns (persistent or
non-persistent) and bind the page to that. This is list B above, and it is the
same shape the reporter arrived at independently for user pickers:
Engineeris a module-owned entity rather thanSystem.User. - Split the page by role. Keep raw System grids on an Administrator-only page. Mendix hides a button to a page the user may not view, so the link simply does not appear.
Related: grant … on System.User is refused by exec — the System module's domain
model is not stored in the project, so it has no access rules to add to. The refusal
is at execution, not at mxcli check, so a script carrying one passes check and
then stops part-way through.
Syntax Reference
Show Commands (Read-Only)
-- Project-wide security overview
show project security;
-- Module roles (all or filtered)
show module roles;
show module roles in MyModule;
-- User roles and demo users
show user roles;
show demo users;
-- Access on specific elements
show access on microflow MyModule.ProcessOrder;
show access on page MyModule.CustomerOverview;
show access on entity MyModule.Customer;
show access on MyModule.Customer; -- a bare name means the entity
-- Full security matrix
show security matrix;
show security matrix in MyModule;
Describe Commands
-- Describe individual roles and users (MDL output)
describe module role MyModule.Admin;
describe user role Administrator;
describe demo user 'demo_admin';
Catalog Queries (SQL)
Security data is available in catalog tables for advanced querying. Use refresh catalog full to populate permissions and role mappings.
-- All permissions (entity, microflow, page, OData access)
select * from CATALOG.PERMISSIONS where ModuleRoleName = 'MyModule.Admin';
-- Filter by type
select ElementName, AccessType from CATALOG.PERMISSIONS
where ElementType = 'ENTITY' and ModuleName = 'MyModule';
select ElementName from CATALOG.PERMISSIONS
where ElementType = 'MICROFLOW' and AccessType = 'EXECUTE';
-- User role to module role mappings
select * from CATALOG.ROLE_MAPPINGS;
select ModuleRoleName from CATALOG.ROLE_MAPPINGS where UserRoleName = 'Administrator';
-- Which user roles have access to a module?
select distinct UserRoleName from CATALOG.ROLE_MAPPINGS where ModuleName = 'MyModule';
-- Describe catalog table schema
describe CATALOG.PERMISSIONS;
describe CATALOG.ROLE_MAPPINGS;
Catalog tables:
| Table | Contents | Build mode |
|---|---|---|
CATALOG.PERMISSIONS | Entity CRUD, microflow EXECUTE, page VIEW, OData ACCESS | refresh catalog full |
CATALOG.ROLE_MAPPINGS | User role → module role assignments | refresh catalog |
Module Roles
-- Create module roles
create module role MyModule.Admin description 'Full administrative access';
create module role MyModule.User;
create module role MyModule.Viewer description 'Read-only access';
-- `or modify` updates an existing role's description instead of failing, so the
-- whole security script stays re-runnable rather than needing a run-once file.
create or modify module role MyModule.ApiUser description 'API consumer';
-- Remove a module role
drop module role MyModule.Viewer;
Microflow Access
-- Grant execute access (multiple roles supported)
grant execute on microflow MyModule.ACT_Customer_Create to MyModule.User, MyModule.Admin;
-- Revoke from specific roles
revoke execute on microflow MyModule.ACT_Customer_Create from MyModule.User;
Nanoflow Access
-- Grant execute access (same syntax as microflows)
grant execute on nanoflow MyModule.NF_ValidateCart to MyModule.User, MyModule.Admin;
-- Revoke from specific roles
revoke execute on nanoflow MyModule.NF_ValidateCart from MyModule.User;
-- Show current access
show access on nanoflow MyModule.NF_ValidateCart;
Note: Security roles persist through DROP+CREATE of the same nanoflow name within a session (by design, for refactor-in-place workflows).
Page Access
-- Grant view access
grant view on page MyModule.Customer_Overview to MyModule.User, MyModule.Admin;
-- Revoke from specific roles
revoke view on page MyModule.Customer_Overview from MyModule.User;
Always Qualify a Module Role
A module role is always Module.Role. The grammar makes the module part
optional, so a bare Admin parses — and then either fails at exec (after every
earlier statement has already been written) or, in create user role, is stored
as .Admin and refused by MxBuild with CE1613. mxcli check reports it as
MDL-GRANT02 without needing a project.
grant Admin on MyModule.Customer (read *); -- ✗ MDL-GRANT02
grant MyModule.Admin on MyModule.Customer (read *); -- ✓
Entity Access (CRUD)
GRANT is additive — it merges with existing access, never removes permissions.
Rights rank None < ReadOnly < ReadWrite and a merge takes the higher, so use
revoke to take access away; a narrower grant will not do it.
A rule is identified by its role set and its where constraint. Two grants
with the same constraint update one rule; with different constraints they make
two, which Mendix combines at runtime (a role's access is the union of every rule
naming it). So adding a where to an existing grant creates a second rule — it
does not narrow the first.
-- Full access (all CRUD + all members)
grant MyModule.Admin on MyModule.Customer (create, delete, read *, write *);
-- Read-only (all members)
grant MyModule.Viewer on MyModule.Customer (read *);
-- Selective member access
grant MyModule.User on MyModule.Customer (read (Name, Email), write (Email));
-- Additive: adds Phone to existing read access (Name, Email preserved)
grant MyModule.User on MyModule.Customer (read (Phone));
-- With XPath constraint
grant MyModule.User on MyModule.Order (read *, write *) where '[Status = ''Open'']';
-- Revoke entity access entirely
revoke MyModule.Viewer on MyModule.Customer;
-- Partial revoke: remove read on specific attribute
revoke MyModule.User on MyModule.Customer (read (Phone));
-- Partial revoke: downgrade write to read-only
revoke MyModule.User on MyModule.Customer (write (Email));
-- Partial revoke: remove structural permission
revoke MyModule.User on MyModule.Customer (delete);
Members added later
A rule also carries a default for members added after it was written, and MDL
derives it from the grant: write * → ReadWrite, read * → ReadOnly, and a
grant written purely as member lists leaves it at None.
So alter entity … add attribute gives the new attribute None on a
member-listed rule. Nothing is broken — every rule gets an entry, the build is
clean — but the attribute renders blank for that role. alter entity warns and
prints the grant that widens it.
What decides this is the rule's default, not how narrow its member list is:
read *, write (Email) is narrower than read *, write * and still picks up new
members, because read * set its default to ReadOnly. Give a rule read * and
narrow with revoke when the role should follow the entity as it grows.
Inherited members
Mendix inheritance is multi-table: a child adds attributes to its parent's, and
all the parent's members belong to the child. Grant them exactly like the
entity's own — read * / write * cover them too:
create persistent entity Docs.DocumentBase (
DocName: String(200),
Confidential: Boolean
);
create persistent entity Docs.Contract extends Docs.DocumentBase (
ContractNumber: String(50)
);
-- DocName is inherited, ContractNumber is Contract's own — name both the same way
grant Docs.Viewer on Docs.Contract (read (DocName, ContractNumber));
-- Attachment inherits the file members from System.FileDocument
create persistent entity Docs.Attachment extends System.FileDocument (
Category: String(50)
);
grant Docs.Viewer on Docs.Attachment (read (Category, "Name", Size));
An access rule must carry an entry for every member, own and inherited —
mxcli writes the ones you did not grant with rights None. Omitting them is
Mendix CE0066 "Entity access is out of date", which masks the CE2729
"No read access to attribute" errors underneath until Studio Pro's Update
security is clicked.
UPDATE SECURITY — reconciling rules that have gone stale
mxcli reconciles an entity's access rules whenever it writes the entity, so a
rule normally cannot go stale through mxcli. UPDATE SECURITY is the repair for
one that did — a model edited elsewhere, or an older mxcli:
update security; -- every module the project owns
update security RestLab; -- one module (IN is optional)
update security in RestLab; -- the same thing
Three things worth knowing, each of which was a defect until mendixlabs/mxcli#1047:
Systemis skipped. Its entities are the platform's, its access rules are Mendix's rather than the project's, and its domain model is not stored in the.mprat all. Naming it explicitly is an error, not a silent no-op.- One unreadable module does not end the run. It is reported and stepped over, so the rest are still reconciled. Previously the first failure returned, and System guaranteed one on every project — which made the command inert.
- The scope is honoured.
update security RestLabused to parse cleanly and run project-wide, because the name reached the parser's error recovery.
A run that skipped something says so. All entity access rules are up to date
means every module was looked at.
The commonest source of a stale rule is a module that arrived from outside
Studio Pro. mxcli marketplace install and mxcli marketplace update copy the
incoming units in verbatim, so a package whose rules do not cover their entities'
members used to land CE0066 in the project with nothing said about it
(mendixlabs/mxcli#1085); both
now run this reconcile for the module they copy in and report the count. Run it
by hand for a module installed some other way, or by an older mxcli:
mxcli -p app.mpr -c "update security UserCommons"
A member name that matches nothing is now an error rather than a silent skip:
Error: entity Docs.Contract has no member(s) DocNam; grant only names members
of the entity or of an entity it inherits from
Exception — user entities. An entity extending System.User is a user
entity, and Mendix manages its inherited platform members (Name, Password,
Blocked, …). Those must not appear in the rule; listing them is CE0066.
Grant only the entity's own members — mxcli leaves the platform ones out
automatically:
create persistent entity Docs.Employee extends System.User (
EmployeeNo: String(20)
);
grant Docs.Viewer on Docs.Employee (read (EmployeeNo)); -- not Name/Blocked
User Roles
-- Create with module roles
create user role RegularUser (MyModule.User, OtherModule.Reader);
-- Create with manage all roles permission
create user role SuperAdmin (MyModule.Admin) manage all roles;
-- Add/remove module roles
alter user role RegularUser add module roles (MyModule.Viewer);
alter user role RegularUser remove module roles (MyModule.Viewer);
-- Remove user role
drop user role RegularUser;
Project Security Settings
-- Set security level
alter project security level off;
alter project security level prototype;
alter project security level production;
-- Enable/disable demo users
alter project security demo users on;
alter project security demo users off;
Guest (Anonymous) Access
Anonymous access is what makes part of an app public — a product catalogue anyone can browse without signing in. It is one flag plus a user role, and the role is the important half: whatever that role can read is the app's public surface.
-- The role anonymous visitors are given. System.User is what lets an
-- unauthenticated session exist at all.
create user role Anonymous (Shop.Viewer, System.User);
alter project security guest access on role Anonymous;
-- Now grant exactly what should be public — and nothing else.
grant Anonymous on Shop.Product (read *);
-- Re-enabling later does not need the role retyped; the stored one is used.
alter project security guest access off;
alter project security guest access on;
Three things worth knowing:
- The role is mandatory. Mendix fails the build with CE0133 ("No user role
for anonymous users selected even though the feature anonymous users is
enabled") when access is on with no role.
guest access onis refused unless a role is given or one is already stored. - Mendix does not check the role exists, so mxcli does. A misspelled role would otherwise build with zero errors and leave anonymous visitors with no access at all — a broken public site that passes every check.
offkeeps the stored role, so toggling access while testing does not lose it. Guest access off with a role set is valid Mendix.
Review anonymous entity access the way lint rule SEC004 asks you to: any
unconstrained read * granted to the anonymous role is readable by the whole
internet (DIVD-2022-00019). Add an XPath constraint or do not grant it.
Demo Users
-- Create demo user (auto-detects entity that generalizes System.User)
create demo user 'demo_admin' password 'Admin123!' (Administrator, SuperAdmin);
-- Create demo user with explicit entity
create demo user 'demo_admin' password 'Admin123!' entity Administration.Account (Administrator, SuperAdmin);
-- Remove demo user
drop demo user 'demo_admin';
The ENTITY clause specifies which entity (generalizing System.User) to use. If omitted, it auto-detects the unique System.User subtype in the project. If multiple subtypes exist, you must specify ENTITY explicitly.
Starlark Lint Rule APIs
Security data is available in Starlark lint rules (.star files):
| Function | Returns | Description |
|---|---|---|
permissions() | list of permission | All permissions across all element types |
permissions_for(qn) | list of permission | Permissions for a specific entity |
user_roles() | list of user_role | User roles with module role assignments |
module_roles() | list of module_role | Distinct module roles |
role_mappings() | list of role_mapping | User role → module role mappings |
project_security() | struct or None | Security level, guest access, password policy |
See write-lint-rules for object property details.
Common Workflow: Setting Up Module Security
A typical security setup follows this order:
-- 1. Create module roles
create module role Shop.User description 'Regular user access';
create module role Shop.Admin description 'Administrative access';
create module role Shop.Viewer description 'Read-only access';
-- 2. Grant entity access
grant Shop.Admin on Shop.Customer (create, delete, read *, write *);
grant Shop.User on Shop.Customer (read (Name, Email), write (Email));
grant Shop.Viewer on Shop.Customer (read *);
-- 3. Grant microflow access
grant execute on microflow Shop.ACT_Customer_Create to Shop.User, Shop.Admin;
grant execute on microflow Shop.ACT_Customer_Delete to Shop.Admin;
-- 4. Grant page access
grant view on page Shop.Customer_Overview to Shop.User, Shop.Admin, Shop.Viewer;
grant view on page Shop.Customer_Edit to Shop.User, Shop.Admin;
-- 5. Create user roles (project-level)
create user role AppUser (Shop.User);
create user role AppAdmin (Shop.Admin) manage all roles;
-- 6. Verify
show security matrix in Shop;
describe user role AppAdmin;
What counts as a member of an entity
An access rule has to cover every member of the entity, or Mendix fails the
build with CE0066 "Entity access is out of date" — a partially covered rule is
worse than no rule at all. read * / write * cover them all, including the
ones that are easy to overlook:
| Member | Named in a GRANT as | Notes |
|---|---|---|
| Attributes (own and inherited) | the attribute name | see "Inherited members" above |
Association, OWNER Default | the association name | on the FROM entity only — adding it to the TO side is itself a CE0066 |
Association, OWNER Both | the association name | on both entities — each end owns it |
createdDate / changedDate | the member name | covered by the rule's default only; Mendix stores no per-member access for them |
owner / changedBy | — | emitted automatically as System.owner / System.changedBy |
Audit members are the one case where naming a member cannot change its rights:
grant R on M.E (write *, read (createdDate)) is refused, because Mendix has no
member access to write for it and a rule that carries one fails CE0066. Let the
rule's default cover it, or change the default.
Common Mistakes
- Creating module roles before the module exists —
create modulemust come first - Referencing non-existent roles in GRANT — create the module role before granting access
- Forgetting qualified names — roles use
Module.Roleformat in GRANT/REVOKE - User roles without System module roles — in Production security, user roles need at least one System module role (CE0156)
- Entity access without proper member rights — use
read *for all members orread (Attr1, Attr2)for specific ones
Validation
After setting up security, verify with:
# check security matrix
mxcli -p app.mpr -c "show security matrix in MyModule"
# Validate with Mendix
mxcli docker check -p app.mpr
Signals
- GitHub stars
- 122
- Forks
- 49
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
manage-security- Source
- github.com/mendixlabs/mxcli