Repository navigation
Releases: mendixlabs/mxcli
Release list
Nightly 2026-10-20
Nightly build from main — 2026-10-20
Commit: 3b24e5a00
Recent changes
3b24e5a Merge pull request #1594 from ako/main
b492d36 Merge pull request #1500 from ako/claude/relaxed-goldberg-q2vra6
369c086 fix(test): point container_rendermode_test at the v2 BSON driver
fa57680 Merge pull request #1497 from ako/claude/relaxed-goldberg-q2vra6
877338e Merge branch 'mendixlabs:main' into main
f82bb02 Merge pull request #1498 from ako/claude/silent-property-drop-issues-55o66g
46836c2 Merge pull request #1590 from ako/main
36f8cc4 fix(test): resolve a microflow's module through the container hierarchy
4ed09e9 test(exprcheck): port the round-trip test off the deleted legacy engine
72c9af8 refactor(deps): move ALTER NAVIGATION's new files off the v1 BSON driver
b86bfba docs(wiki): two silent-property-drop insights from the 2026-10-20 fixes
9e0a76b fix(check): point a column's Filterable: at the filter widget slot
1db5b0f Merge remote-tracking branch 'origin/main' into claude/relaxed-goldberg-q2vra6
f8c0987 refactor(deps): move off the deprecated v1 BSON driver entirely
011e858 fix: write a pluggable widget's top-level expression properties
4a3571d fix: describe and alter a container's RenderMode; refuse unknown members
4c0e494 Merge pull request #1495 from ako/feature/alter-navigation-menu-items
a79eff4 Merge pull request #1494 from ako/integrate/wave2-upstream-prs
87b4cbc docs(navigation): fence the navigation synopses as text
fd57cb6 docs(skills): one row per user_role field; trim a blank line instead
This is an automated pre-release build. Use tagged releases for production.
v0.27.0
[0.27.0] - 2026-10-15
Headline: exec is all or nothing: when a statement fails at write time, the whole run is rolled back, so a script never leaves a project half-changed. Alongside it: a wave of describe → check → exec fixes from round-trip runs over real apps (the Siemens FSM template, the GenAI showcase, Marketplace modules), Mendix 9 projects can take microflows, nanoflows and pluggable widgets again, check -p resolves many more of the references exec and the build would refuse, the classic filemanager widget and role-based Visible: roles (…) are in MDL, and the companion draws a module's domain model. The MongoDB driver is upgraded for the October 2026 BSON advisories.
Added
mxcli search --fail-on-matchturns the search into a gate — a pipeline that must refuse a change when a forbidden word turns up in the project's text had no exit status to read:searchexited 0 whether it found something or not. With the flag it exits 1 when there is at least one hit and 0 when there is none, printing the results as before (a JSON array under--json). A search that cannot run, such as an unbalanced"or an unknown project, exits 2 so that 1 always means a match; theSEARCHstatement still answers an unrunnable query with "No matches found.". Without the flag a hit exits 0 as before. Rules: docs-site "Full-Text Search".filemanager— the classic file manager (Forms$FileManager) in MDL (#151) —filemanager fileManager1 (Label: 'File', Type: Upload, MaxFileSize: 10, AllowedExtensions: '.xlsx', ShowFileInBrowser: false)inside a data view on aSystem.FileDocumentspecialization. Before, there was no way to author one:pluggablewidget 'Forms$FileManager'had no definition, the bare keyword was refused as MDL-WIDGET25, anddescribe pageprinted a stored one as a NOT re-executable comment, so describe → exec dropped it. Omitted properties take Studio Pro's defaults (Both, 5 MB, any file, false); the written BSON has the property set of a Studio Pro-authored file manager.alter page … setchanges all four properties. A file manager in no data container is MDL-WIDGET45 atcheck, and one in a data view on an entity that is not a FileDocument is refused byexec— both were mxbuild's CE6810 after a cleanexec.- A module's domain model as SVG, in the companion and on the command line —
mxcli describe --format svg domainmodel <Module>writes a finished SVG: entities at the positions the model stores, coloured by kind, with attributes, associations, multiplicities and generalizations. Boxes are sized to fit every name, pulling the stored layout apart just enough instead of clipping. In the Claude Code companion, selecting a module's Domain Model node in the desktop app or VS Code shows it at actual size, with pan, Fit and a full-width view; the terminal lists the entities as before. Visible: roles (Mod.A, …)— role-based widget visibility (#1460) — Studio Pro's "visible for module roles" was never read bydescribe, had no MDL spelling, was written empty and was dropped byalter … set Visible. It is now read, written and combinable with an expression or attribute condition, on widgets, tab pages and navigation list items. A tab page's (#1465) and a layout grid row'sVisible:(#1461) are now written too;describe→execused to make a Studio Pro row always visible.+=/-=for reference-set members insidechange(#1473) —change $Obj (Name = 'x', Mod.Assoc += $Item): a Change object activity that mixes an Add or Remove with other members had no spelling, anddescribeprinted the Add as=, which replaces the set.- ALTER PAGE addresses navigation list items and widgets inside pluggable widgets —
nav item('Home'),nav item(t1)andnav item@2name an item by its text, a widget in it, or its position;insert into navList1 { item … }adds one (#1388). Widgets inside an Accordion group or a pop-up menu item, legacy DataGrid columns and search fields, and a file manager's own properties can be set and dropped (#891, #1459, #151).setalso writes a text box'sPlaceholder,PasswordandValidation(#414), a static image'sImage(#1464), and aLabelon an input that showed none (#292). Image: $currentObjectbinds the Image widget to an uploaded image (#1476) — no MDL form bound the pluggable Image widget to the object in context; it is refused outside a data container or over an entity that does not extendSystem.Image.describe module --all(#1435) — thedescribe modulesubcommand printed only the module and its roles although the help promised "all contents";--allrunsdescribe module … with all, and the help says what each form does.with allnow emits every document type, where it skipped nanoflows, snippets, JavaScript actions, mappings, task queues and image collections without an error (#1421).mxcli test --app-port / --admin-port / --serve-port—test --localhard-coded its ports, so a machine with 8081 taken could not run tests. A taken port fails up front, naming the port, its role and the flag that moves it.mxcli layout --only-overlapping(#1479) — and a new entity without@Positiontakes the first grid slot that clears every entity and annotation in the module, so an entity with many attributes no longer overlaps the one below.- SEC010 flags page data widgets their roles cannot read — a list view, data grid, reference selector or pluggable combo box over an entity none of a page's roles can read renders empty, with check, mxbuild and the runtime log all clean. SEC010 now asks, per user role that can open the page, whether any of its module roles has a read rule, using the System module's access rules measured from a built model, and reports a System.User list that only ever shows the current user.
mxcli syntax microflow workflow-actions— a topic for the microflow statements that start and manage workflows.
Changed
- mxcli no longer creates a module role on its own (#1474) — a new page, microflow or nanoflow in a module with no module roles used to create
<Module>.User("Auto-created default role for mxcli document access") and grant the document to it, so a security role appeared that nobody chose, open to every user role later mapped to it. A new document now starts with no allowed roles in every module, andmovemaps a document's roles to same-named roles of the target module without creating any. Breaking for scripts that granted to<Module>.Userwithout creating it: addcreate module role <Module>.User;—check --referencesandexecboth report "module role … does not exist", and MDL-SEC21 names the fix for a flow a page uses. Roles and grants an earlier mxcli created are left as they are;create module role M.Userstill adopts such a role. check -presolves many more references beforeexecwould refuse them (#1472) — each of these passedcheckand failedexecor the build (CE1613, CE0174):- ALTER PAGE targets and the flows, pages, snippets and entities inserted widgets name, and DataGrid 2 column attributes against the grid's entity;
- the members an access rule's
wherenames (case-insensitively, as Mendix does), an entity event handler's microflow, enumeration keys named in expressions, and the entities a view entity's OQL reads from; - image references against image collections the script creates (#1419), and calls to flows the script creates, including mutually recursive ones, which no statement order could check before (#1405);
- object-list item properties that would be dropped (
group g1 (Caption: …)in an Accordion, whose header isheaderText), and a file manager outside aSystem.FileDocumentdata view (CE6810); - a
changeor member path over a list retrieve (MDL-RETRIEVE03, #1422),show message … type ok(MDL-SHOWMSG01),import from mappingwithout a result entity, which made the project unloadable (#1418), andalter settings JavaVersionon a Mendix 9 model, which stores none (#1438).
- A multi-file
checkends on a verdict for the whole set (#1431) — "N of M files failed: …" or "All M files passed", where the last line used to be the last file's verdict, which could read "Check passed!" with exit code 1. - Commands without
-pname the project they auto-detected (#1444) — the notice gives the absolute path and how to choose another, and with several.mprfiles in the directory it lists them and asks for-pinstead of only saying-pis required. - Studio Pro document names in
describe --helpand the catalog (#1395) —consumedrestservice,publishedodataservice,taskqueue,aimodel,appsecurity, …, and catalog viewsconsumed_rest_services,published_odata_services,task_queues, …. Every old keyword and view name still works. mxcli syntaxresolves statement verbs and stops teaching deprecated spellings (#1389) —mxcli syntax grant(andrevoke,refresh) found no topic, and several examples used spellingscheckflags as MDL-DEPRnnn.- One-shot mxbuild runs have a timeout and a stall watchdog —
mxcli newanddocker buildcould hang forever behind a Gradle build that stopped making progress. They now stop afterMXCLI_MXBUILD_TIMEOUT(30 minutes) orMXCLI_MXBUILD_STALL_TIMEOUTof silence (10 minutes), printing the last output lines and the Java home used. mxcli inittitles the generated docs after the app, not the repository folder, and the bootstrap script defau...
v0.26.0
[0.26.0] - 2026-10-11
Headline: Documents mxcli writes now survive Studio Pro's merge and diff: every element carries exactly the property names Studio Pro writes for the project's Mendix version. Before, a key mxcli left out, or wrote for a newer version, made a document unmergeable the first time Studio Pro saved it, although mx check, the build and the runtime stayed green. mxcli fix property-sets repairs projects an older mxcli wrote. Alongside it: a Claude Code companion mod with a Project pane, describe → exec round trips that keep loop back-edges, shared error handlers and page design properties, check -p resolving script sets and security references the way exec does, mxcli run --local as a background service, and a run of Windows fixes for the local dev loop. The language poster puts all of mxcli and MDL on one page.
Added
alter page/alter snippetadd and drop parameters (#1234) —add parameters $Order: M.Orderanddrop parameters $Order, besideadd variables/drop variables. Before,add parameterswas a parse error ("mismatched input 'parameters' expecting VARIABLES_KW") and the only route wascreate or replace page, which rebuilds every widget and loses whatdescribedoes not round-trip. The declaration is CREATE'sParams:rule, written by the same converters (so the 11.5+IsRequired/DefaultValuegate holds). A page with aUrlmust name the new parameter in it —set (Url: …)in the same statement — or exec refuses with the URL to use (mxbuild CE5601, measured on 11.12.1); a snippet parameter must be an entity (MDL087, at check time too); a drop is refused while a data source or expression still uses the parameter.mxcli git note— Studio Pro's commit metadata for commits made with git, and theteamserver-gitskill (#1337) — Studio Pro attaches a note underrefs/notes/mx_metadatato every commit (Mendix version, branch, and the model units the commit changed) and reads it to check a revision's version and show what it changed; a commit an agent made with git had none, Studio Pro 11.13 back-filled an"(unknown)"placeholder, and the user saw conflicts and changes the change pane did not show.mxcli git note -p app.mpr [--write]computes the note from the commit — the.mprunit index andmprcontentsat the commit and its parent — for the commits not yet pushed (or the ones named), keeps any note Studio Pro wrote (--forcereplaces it) and replaces placeholders. Measured against the 8 notes Studio Pro wrote on a Team Server repository's MPR v2 history: the header is identical on all 8 and the change list identical on every ordinary commit (Added/Modified/Deleted/Moved, a deleted module listed once,$ID-only re-serialisation not listed); on a version upgrade, which Studio Pro does and annotates itself, it lists more units than Studio Pro. The newteamserver-gitskill teaches the workflow: commit from Studio Pro when it is open, create branches on the Team Server (a local-only branch crashes Studio Pro 11.13 on open), pushrefs/notes/mx_metadatawith the branch before Studio Pro's background fetch discards it, and never merge, rebase or force-push model history with git.languages()in Starlark lint rules — the languages enabled in the project (#1306) — a rule had no way to tell which languages are enabled:strings()has a row for every stored translation, and a fresh en_US-only app already carries nl_NL texts, so enabling a language changed nothing a rule could see, andlanguages()failed to load withundefined: languages. It returns onelanguageper enabled language, from the project settings, withcode,is_defaultandcheck_completeness, so a per-language check (every page title translated into each enabled language) can filterstrings()to the enabled codes. It reads the project settings, not the catalog, so it needs no full catalog build; a failed settings read fails the rule instead of answering[].- A published REST service's authentication (#1331) —
create [or modify] published rest service M.Api (…, Authentication: (basic, session, microflow M.Authenticate))sets Studio Pro's Requires authentication and its methods:basic(username and password),session(active session),microflow M.F(custom);Authentication: noneis "Requires authentication: No". The methods are stored in the order written, which is how Studio Pro stores the ones ticked, anddescribeprints them in the stored order (it omitsnone). Left out,create or modifyandalterkeep the stored setting, as before.alter published rest service M.Api set ( Key: value, … )now takes create's property list (Path,Version,ServiceName,Authentication);set Key = '…'still works. The authentication microflow must returnSystem.Userand take only aSystem.HttpRequestand/orSystem.HttpResponse(Mendix: CE0334, CE0336, measured withmx checkon 11.14.0);execandcheck --referencesrefuse anything else as MDL-REST04. Executing thedescribeoutput of each of ako/TestApp's Studio Pro services writes nothing. send email— the built-in Send Email activity (Mendix 11.13+, beta) —send email ( From: …, To: …, Subject: 'Order {1}' with ({1} = …), Body: template '…', HtmlBody: template '…', Headers: ('X-Name': 'value'), Attachment: $Doc, Host: …, Port: …, SecurityType: ssl, CheckServerIdentity: true, ConnectionTimeout: 30000, Authentication: basic (Username: …, Password: …) ) [on error …];creates aMicroflows$SendEmailAction, which sends SMTP mail without the Email Connector module. The settings are one property list (ADR-0013), keyed by the metamodel's names; an unknown, repeated or misshapen key is an error, and so is a missing From, Host, Port or recipient (mxbuild: CE0166).describe microflowrenders the activity instead of-- Unsupported action: Microflows$SendEmailAction, and the description re-executes to the same activity.checktype-checks its expressions (E009: String addresses, host and credentials, Integer/Long port, String template parameters — what mxbuild reports as CE9528/CE0117), and warns on a server-identity check without SSL and on header names Studio Pro would refuse (MDL-EMAIL02/03). A 10.x–11.12 project is refused; a stored activity MDL cannot restate (authentication document, pre-11.13 message) still describes as unsupported rather than being rewritten smaller.mxcli syntax microflow.send-email. (#1315)mxcli playwright check— a text verdict for pages of a running app, in one call —mxcli playwright check /p/a /p/b -p app.mprloads each page in one headless browser and prints the verdictrun --page-checkprints (title, heading, rows, visible text, error banners, console errors) plus failed same-origin requests and the HTTP status, thenOK n page(s)orFAIL k of n page(s). Exit status 0 all passed, 1 a page failed (HTTP error, sign-in form or a 401 instead of the page, an error banner or error dialog, a console error, a failed request, a failed assertion), 2 the check could not run. It signs in when needed —--user/--password,--role R(the project's demo user with that user role), or with only-pa demo user — saves the session under.mxcli/playwright-check/, reuses it on the next check and renews it when the runtime has restarted.--assert-text,--assert-count 'SELECTOR>=N', and--screenshot out.png, which prints the path and never the image. It replaces the hand-written playwright-cli login/goto/sleep/eval/screenshot sequences that were about a sixth of the tool calls in a measured app-building session, each ending in a PNG read; thetest-appandverify-in-runtimeskills and/mendix:testnow route "check a page" to it.mxcli run --localruns as a background service —--detachstarts the warm loop in its own session and returns once the app serves (running: http://127.0.0.1:8080/ (pid N, watch, log .mxcli/run.log)), or prints the boot failure and the last log lines and exits 1.mxcli run statusprints one line (URL, uptime, last build and how it was applied, a pending change; a dead pid is reported as stopped).mxcli run waitblocks until the model as it is now has been applied —applied: build #3 via reload in 1.4s— or failed, with the CE codes; it is race-free (a change already applied when it starts is reported at once), somxcli exec x.mdl && mxcli run waitis one honest call;--readywaits for the boot.mxcli run stopshuts the run down and then sweeps its process session, so no orphaned runtimelauncher or mxbuild JVM is left, even from a run killed with-9.mxcli run restartstops and detaches again with the same arguments. Exit codes: 0 ok, 1 failed, 2 timeout, 3 not running, 4 starting. A second run for a project that already has one is refused. These replace thenohup/until grep/pkillloops that took about a quarter of all tool calls in measured agent sessions; the run-local and run-app skills and the generated CLAUDE.md now teach them.mxcli diag session-report— where an agent session's calls and tokens went, from its Claude Code transcript —diag loop-reportcounts mxcli processes; this reads the transcript (~/.claude/projects/<project>/<session>.jsonl, subagent transcripts included) and reports model calls, tool calls by tool, cache-read / cache-write / output tokens and wall time; Bash calls split into mxcli verbs, playwright, build, git and other; every call in one category (orientation, write, validate, apply, verify, diagnosis, retry, delegate, other); the costliest results by re-read cost (size × the model calls after it, up to the next compaction); error → retry chains aggregated by normalised error; and repeated file reads and syntax/help/skill lookups. Several transcripts give ...
v0.25.0
[0.25.0] - 2026-10-05
Headline: mdl 1 is frozen: the beta language is now a contract, and describe, the REPL and -c speak it by default. A dozen syntax rounds (R2–R12) settle on one spelling per construct — properties in ( ), children in { }, Param = expression at every call site, bare expressions, list for every listing — and strings and expressions are stored exactly as Studio Pro spells them. Script files without a header are still read as mdl 0, and fmt --upgrade migrates them. Alongside it: check -p predicts many more of the refusals exec and the build would hit, re-running an unchanged script writes nothing, lint rules gain catalog builtins over loop bodies, widgets and associations, mxcli fix hashes repairs the MPR v2 ContentsHash index, and Mendix 11.15's message definition documents and mapping shape are supported.
Added
- Message definitions on Mendix 11.15:
create message definition(ako#987). Mendix 11.15 replaced the message definition collection with one document per definition, and Studio Pro's converter turns a collection into a folder of that name holding the documents. The new statements cover the document form:create [or modify] message definition M.Name [folder '…'] for M.Entity [as '…'] { members }(the collection'sdefinitionentry on its own),describe message definition,drop message definition,list message definitions [in M]andalter message definition M.Name add|drop|set member …. A mapping names the document aswith message definition M.Name. A three-partM.Collection.Definitionfrom an older script still resolves on a converted project and is stored asM.Name. Version-dependent: the document form is refused below 11.15 with a hint to use the collection, and on 11.15 the collection statements are refused with a hint to use the document form.list message definition collectionson 11.15 points tolist message definitions.describeprints the document form, so describe → exec round-trips. Before this, 11.15 projects showed no message definitions, and a mapping over one failedmx checkwith CE0270. This is an addition, somdl 1scripts are unaffected. checknotes a locale-lessformatDecimalin hand-built JSON (ako#982) — MDL-JSONNUM01 (info) forformatDecimal(x, '0.00')inside a+concatenation whose string literals contain{or":, which writes12,50for a Dutch user. UsetoString(round(x, 2))or pass a hyphenated locale. A display string ('Total: ' + formatDecimal(…)) is not flagged.mxcli fix hashesverifies and repairs the MPR v2 ContentsHash index (ako#972) — everyUnit.ContentsHashin the.mpris compared withbase64(SHA-256)of its.mxunit, and mismatches, missing files and orphan files are reported; the command exits 1 when any remain.--repairrewrites the mismatched hashes from the files in one transaction, under the same Studio Pro open-project guard as every write. Restoring a unit withgit checkoutleaves the index stale andmx checkdoes not notice; measured on a TestApp copy, an entity added byexecand then reverted with a byte-level restore is reported as oneDomainModels$DomainModelmismatch, and after--repairthe verify is clean andmx checkreports 0 errors.execanddocker checkprint a one-line warning when the index disagrees (about 0.2 s on 900 units). An MPR v1 project has no index, and the command says so.- Warnings for git states that crash Studio Pro (ako#972) — Studio Pro 11.13 fails to open a project ("Unable to find 'system' property in 'system'") when its git branch has no upstream or git reports "detected dubious ownership".
docker check,run --localand the newmxcli diag -p app.mprproject section warn with the remedy (git push -u origin <branch>;git config --global --add safe.directory <path>on the Studio Pro machine). Never fatal, and silent outside a git repository, on a detached HEAD, underCI, or withMXCLI_NO_GIT_WARNINGS=1. See the new "Working Outside Studio Pro" page. create translations … without marketplace, and an unscoped translations run warns when it writes into Marketplace modules (ako#970) — withoutin <Module>,create [or modify|or replace] translationsreaches the whole project, Marketplace modules and their Atlas page templates and building blocks included; on TestApp'Cancel' as 'Annuleren'changed 35 Marketplace documents of 38. A module update replaces those modules, so the translations are lost at the next update and show up as unexpected diffs until then. The run now warns with the count per module and how many are templates or building blocks.without marketplace(also ondescribe translations, which emits it) keeps the run, and anor replacedeletion, to the app's own modules and names the file's entries it left alone. The default is unchanged: what an existing script writes does not change underneath it (ADR-0011).checkforesees CE8812 for a list over a single-object association (ako#969) — MDL-ASSOCDS01: a list view, data grid or gallery whose data source is$currentObject/M.Assoc(orassociation M.Assoc) over a Reference followed from its FROM entity, or over a Reference with owner Both from either end, is refused; mxbuild 11.13.0 and 11.14.0 reject both with "A grid association path must result in a list". A ReferenceSet and the reverse of a default Reference are accepted, as mxbuild accepts them.- An info hint for a quoted template parameter that reads like an expression (ako#969) — MDL-PARAMQUOTE01:
{1} = 'formatDateTime($Log/Date, ''d MMM'')'is stored as literal text and the page shows it verbatim (measured withrun --local);checknow says so and suggests dropping the quotes. It fires on a quoted call of a known Mendix function (parenthesis right after the name) or a$Var/Attrpath, and not on text such as'Total (incl. VAT)','length (cm)'or'Price in $'. - A
commitreference kind in the catalog (ako#963) —refshas acommitedge from a flow to the entity it commits: a commit action, or a create / change that commits (YesorYesWithoutEvents), the latter beside itscreate/changeedge. The entity of a committed variable is resolved likechange/delete, loop iterators included, socommit $Orderinsideloop $Order in $Ordersnow has a row.refs_to("M.Order")in a Starlark rule answers which flows commit an order.commitis not in the analysis graph and not a caller kind. The catalog schema version is bumped, so a cached catalog rebuilds. total_activity_counton the Starlark microflow struct (ako#963) — the catalog'sTotalActivityCount(loop bodies included, at any depth) for every flowmicroflows()yields: microflows, nanoflows and rules.activity_countkeeps counting a loop as one activity.- The catalog's
widgetstable records the widget tree, appearance and primary action (#1268) —ParentWidgetId(the nearest catalogued ancestor; skipped wrappers, layout grid rows and columns, tab pages and data grid 2 columns are transparent),Depth(0 at the page or snippet root),Class,Style,DynamicClasses,ActionType(the stored type of the button, on-click or click action, e.g.Forms$DeleteClientAction) andHasConfirmation. Starlarkwidgets()exposes them asparent_widget_id,depth,class_name,style,dynamic_classes,action_type,has_confirmation, pluspage_ref, so a lint rule can flag deep nesting, inline styles, classes outside an allow-list and delete buttons. A delete action has no confirmation setting in Mendix; the enforceable rule is "no button uses the delete action directly". The catalog schema version is bumped, so a cached catalog rebuilds. - A project records which mxcli wrote its tooling, and an older binary says so (ako#952) —
mxcli initand everyinit --sync-skillswrite.ai-context/mxcli-tooling.json(version, build time, date; rewritten only when the version changes). Any command that opens the project with-pand a binary older than the stamp warns once on stderr, naming both versions and how to update;init --sync-skillsfrom an older binary refuses instead of rolling the skills, rules and CLAUDE.md back. Releases compare by number, nightlies by tag date, a release against a nightly by build date; dev builds are never reported. Binaries from v0.24.0 and earlier cannot read the stamp, so the regenerated.claude/bootstrap-mxcli.shchecks it before choosing a binary: an oldermxclion PATH is not linked in (it downloadsMXCLI_TAGinstead), an older./mxcliis replaced, and the download lands through a temporary file so a./mxclisymlink never has it written through into the PATH binary. init --sync-skills(alias--sync) refreshes the bundled lint rules and the mxcli section of CLAUDE.md / AGENTS.md (ako#952) — it used to refresh only the skills, so a project kept the lint rules and guidance of whichever mxcli first initialised it. Bundled rules are recognised by file name; your own rules beside them are never touched. CLAUDE.md and AGENTS.md are now written between<!-- mxcli:begin … -->/<!-- mxcli:end -->markers, and only that section is refreshed — by the sync and by a re-run ofmxcli init— so project notes outside the markers survive. A file written before the markers is left alone by the sync, with a note; runmxcli initonce to adopt them.mxcli initandmxcli newcreatemdlsource/(ako#952) — the directory the generated CLAUDE.md says scripts live in, with a README.mxcli report --modules A,B(ako#953) — scores only the named modules, aslint -mlints them: findings outside the selection, including project-wide ones, are not scored, and the report names the selection.- **Starlark lint builtins over the catalog tables that had ...
v0.24.0
[0.24.0] - 2026-09-24
Headline: An element's storage GUID is the database's identity, and mxcli now treats it as one. A production report of 28 attributes emptied across 607 rows by a single edit (#1119) traced to five write paths that re-minted GUIDs — one of them moving 282 in a single module. They are fixed, and a new guard at the write choke point refuses any write that moves one: a class of data loss that leaves the model valid, mx check clean and DESCRIBE byte-identical, and surfaces only when the package meets a database that already holds data. Alongside it, MOVE ENTITY and RENAME stop leaving a project unbuildable, and four more scripts that passed every gate and failed the build are refused.
Added
-
A write that moves an element's storage GUID is refused — the guard behind the data-loss fixes below. Changing a
GUIDwhile keeping the$IDis data loss with nothing to see: the model stays valid,mx checkpasses, the build succeeds, andDESCRIBEis byte-identical because the reader never surfaces a GUID. It becomes visible only when the package meets a database that already holds data. None of the three guards already at the write choke point could catch it — no-op elision compares the document with itself, and the identity-decision test sees only the codec'sFreshGUIDFields. -
mxcli run --local --page-check(#614) — answers in text what a screenshot answers in pixels: did the page render, is there an error banner, did the grid get rows. A PNG costs ~1,500 tokens to read against ~100 for a verdict, and an image read into a conversation is re-read by every later model call, so one screenshot early in a long session is charged hundreds of times. It is also the weaker instrument for this question, measured against two real rendered pages. -
mxcli diag loop-report— where a session's mxcli calls actually went. Every invocation already wrote asession_startnaming its argv (mdl/diaglog, wired atnewLoggedExecutor, so it covers all commands rather than a curated subset) and nobody had ever read it. Reports per-command call counts, wall time and median, pluscheckimmediately followed byexecof the same script. The instrument first, because every efficiency lever after it is a hypothesis until a number moves. -
ALTER PAGE … SET '<design property>' = <value> ON <widget>— a design property lives inAppearance.DesignProperties, soSETdead-ended on it andALTER STYLINGwas a second statement for the same operation. Both spellings now work. -
ALTER PAGES [IN <module>] SET '<design property>' = … WHERE WIDGETTYPE = <keyword|'widget id'> [DRY RUN]— the bulk form. Applying a styling decision across an app ("every data grid gets Compact and Striped") previously meant one statement per widget per page, which is how a design system drifts. The selector resolves an MDL widget keyword to the widget id. -
Workflow Groups are authorable (#272) —
alter settings workflows add [or modify] group 'Approvers' (Description: '…'), plusmodifyandremove, and they appear inDESCRIBE SETTINGS. They are project settings, peers ofUserEntityandDefaultTaskParallelism, and were missing from both — so a scripted project setup had to stop and open Studio Pro to define the buckets a user task's group targeting selects from. -
A sort column can name the association it navigates —
describeemitted only the sort attribute's qualified name, so replaying it had to re-derive the hop, and where two associations reach the same entity there is nothing to derive it from. Measured on 11.12.3 withOrder_ShipToandOrder_BillTobothOrder → Address: a microflow sorting by the billing address came back through describe → exec sorting by the shipping one. -
Two more Starlark surfaces for custom lint rules —
rest_clients()andrest_operations()builtins (the catalog built both tables, includingrest_operations.Timeout, and neither was reachable — so "REST/web services should have a timeout" could not be written as a rule at all), andservice_ref/action_refon the activity, so a rule that finds aRestCallActioncan now learn what it calls. -
A run's repeated
Unchangedreports collapse into one line. Measured on a 40-statement script applied to a project already holding it: 41 lines / 1,604 B → 2 lines / 177 B. Cheap in a terminal and charged repeatedly in an agent session, where a tool result is written into the conversation once and re-read by every later model call.
Changed
-
A data grid's column filters are documented where a data grid is documented — the filter widgets section of the
create-pagewidget reference opened with "Filter widgets are used inside GALLERY FILTER containers", so a reader building a grid found the widgets only by reading a gallery section to the end. It now names both homes up front, with the grid form first. Theoverview-pagesskill gains the two forms its type table did not cover: adropdownfilterover an association (Association:+datasource:+CaptionAttribute:, verifiedmx check0 errors and round-tripped throughdescribe),FilterType: betweenfor a date range, and the reminder that a grid filters itself — two generated apps built a filter bar over a non-persistent helper entity instead, one of them costing three microflows and 1,100 characters of XPath where five column filters would do. -
Microflows written without
@positionare laid out to be read, not just to be valid (#1154). Geometry and connection sides only: no MDL syntax changes, a statement carrying@positionis never moved, anddescribe→execstill reportsUnchanged microflow. Measured on a generated app of 41 microflows, none with an@position: the widest flow went from 6930×160 px on one row to 3220 px, with 0 overlapping elements andmx checkat 0 errors before and after.- A long main line wraps onto rows. Past 2880 px (two Studio Pro canvases) the next statement starts a new row under everything the current row occupies — branch lanes, loop boxes and notes included, measured after they are built rather than estimated. The line joining two rows leaves the bottom of the last element and arrives on top of the first, so it runs through the band between the rows instead of back across the activities. A tail short enough to finish within two more activities stays on its row rather than becoming a stub underneath.
- The main line no longer waits for a guard's branch.
if … then …; return; end ifdraws its branch in the lane below and ends it there, yet the next element used to be placed past the branch's far end: 370 px from the split, against 40 px between any two activities. It now stands in the column the branch starts in, and only an element that reaches down into that lane — another decision, a loop, an activity with an error handler — waits for the lane to clear. A merge stands one ordinary gap after its branch (was 120 px), a branch ending inreturnis measured with the end event it draws, and a loop box keeps the same 40 px gap from its neighbours as an activity. - An enumeration split of four or more cases draws its lines in three groups — the upper third from the split's top corner, the middle from its right, the lower from its bottom, every one arriving on the left of its activity, mirrored on the merge. The anchor pair on these flows is where the
caseorder is stored, and past the third case the table behind it used sides no drawing would choose: case 4 left the split's left corner, cases 5–8 arrived on top of their activity.describenow reads the order as side, then position on the canvas; a model written with one pair per case reads exactly as before, and a split of up to three cases is unchanged. - A note sits above its element, not inside it. Notes were offset from the element's centre, which is inside any loop box; they are now placed above its top edge, and the connector runs from the bottom of the note to the top of the element instead of top to top.
Fixed
-
An input inside a list view rendered disabled with every gate green (ako#631) — a
listviewthat does not sayeditable: trueis writtenEditable: false, and the list view's read-only context wins overeditable: Alwayson the input, including inside a nested data view. The BSON is valid,mx checkis clean and the build succeeds; only the running app shows it. The default is not changed: false is Mendix's own (mendixmodelsdk 4.115.0,Pages$ListView.editabledefaults to false).mxcli checknow warns MDL-WIDGET31 on a list view that will be written read-only while it holds an input not markededitable: Never, and nameseditable: trueas the fix. A quotededitable: 'true'is a string and is written false, so it warns too. -
A queued call to a microflow that returns a value passed
mxcli check --referencesand failed the build with CE7033 (#1064) —call microflow M.F() in queue M.QwhereFreturns Boolean said "Check passed!", andmx checkthen reported[CE7033] "A microflow used for background execution must have a Microflow return type of 'Nothing'.". Both the call and the queue resolve; the constraint is on the flow the call names. MDL088 now reports it — with no project when the script creates the microflow itself, and under--referencesfor one already stored. Measured on mxbuild 11.12.0: a Boolean target → CE7033, the same call on a void target → 0 errors. -
Two bundled lint rules reported nothing, on every project, since they were written —
ARCH002(entities without a data-change microflow) andARCH003(entities without a business key) both skip an entity withif entity.entity_type != "PERSISTENT". The catalog storesPERSISTENT, butLintContext.Entitiesnormalizes the kind toPersistentbefore a Starlark rule re...
v0.23.0
[0.23.0] - 2026-09-21
Headline: The last workflow constructs only Studio Pro could author become MDL, describe → exec stops losing what it just read, and the legacy engine is gone. Workflow event sub-processes, notification events, multi-user completion rules and AI agent tasks are authorable; a wave of page round-trip fixes closes the gap between what mxcli reads and what it writes back — a password field that came back as a plaintext text box is the one to read first; and sdk/mpr is deleted, leaving one engine behind the backend abstraction.
Added
-
marketplace install --file <package.mpk>— installs a.mpkthat is already on disk through the same writer as an online install, with no marketplace lookup and no PAT. The kind is read from the package's ownpackage.xml: a module goes through the transplant writer (storage format preserved, theme modules included), a widget lands inwidgets/, anything else is refused rather than guessed at.--fileand a content id are mutually exclusive, and--versionis refused with--filebecause it selects a marketplace release.This closes the gap the command's own help used to point at:
marketplace downloadcould fetch a package to disk, after which the only routes were Studio Pro ormx module-import— the two paths the help itself explains are worse (module-import rewrites MPR v2 as v1 and refuses theme modules). Every function the online path runs after its download (PackageProject,PerformInstall,InstallPackageFiles,moduleNameFromMpk) already took a plain path;--filehands them one.A module installed from disk carries no marketplace version stamp, because it has no marketplace identity:
PerformInstallskipsStampMarketplaceVersionwhen the version id is empty, soupdateanddiffreport — correctly — that no marketplace content is installed under it, instead of claiming a release while naming none.Field run (Linux, no Studio Pro, mxbuild 11.14.0): a module exported with
mx create-module-packagefrom one Mendix 11.14.0 app, installed with--fileinto a fresh MPR v2 app named by a relative-p— 4 units copied, 6 bundled files installed,mprcontents/intact,SHOW MODULESlists the module with an empty Source column,mx checkreports 0 errors; a second run reports "already installed" and changes nothing. A Mendix 10.6.4 theme package is refused withmx's own message (the reference project is still built bymxat the project's version, somx module-import's window applies) and the app is left untouched. -
Named datasources on pluggable widgets (#1109, follow-up to #643) — a datasource-typed property can be given under its own schema key (
optionsSourceAssociationDataSource: database from Module.Customer) instead of only through the genericdatasource:clause, so a widget exposing several datasources can be given each of them. A mode gated onhasDataSourceis selected by a named datasource too, and the newhasDataSource:<propertyKey>condition says WHICH datasource selects a mode — the legible form for a widget whose modes are exclusive by that (a ComboBox's association vs database), where barehasDataSourcecannot tell them apart and mode order silently decides.A dependent property binds against ITS datasource's entity: the link is the widget package's own (
widget.xml'sdataSource="…"), not mapping order, so a DatagridDropdownFilter'srefCaptionresolves againstrefOptions' entity and itsattragainstlinkedDs'. Verified end to end on a real project: both datasources persisted and the two attributes stored asNamedDS.Order.NumberandNamedDS.Customer.Name.#643's guarantee is unchanged, restated in terms of the value rather than the key: a real datasource under a datasource-typed property is accepted, a scalar there is still MDL-WIDGET05 — it names an entity, cannot be stored as a datasource, and used to pass
checkandexecand fail the build with CE0642. Aliases are covered, so the scalar cannot reach the drop under the other spelling. MDL-WIDGET16 accepts a named option list too; while it read only the generic clause it contradicted MDL-WIDGET05 on the same widget, which blockedexecentirely.On a widget exposing several datasources the generic
datasource:clause is refused, naming the keys to use — previously it filled whichever datasource-typed property Go's map iteration yielded first, so the same script produced different documents from run to run with no error either way.The same per-datasource entity context now applies to object-list items, which run their own resolve loop: a chart series given both a static and a dynamic datasource used to bind its static x/y attributes against the dynamic one's entity, writing a reference to an attribute that entity does not have (measured:
CH.Forecast.Region, CE1613 ×2 from mxbuild 11.6.6). The link was already in hand —ItemPropertyMapping.DataSourcecarries widget.xml'sdataSource="…"and the generated chart definitions already mapstaticDataSourceanddynamicDataSourcewith every dependent's link — it simply was not read. A series configuring one datasource, which is every series in every real chart sincedataSetselects static or dynamic, resolves exactly as before.A linked datasource is refused: a widget.xml
isLinked="true"property is filled from the containing widget (a Data Grid 2 supplies its column filter'slinkedDs), so a.def.jsonmapping one is rejected at build time rather than written. Measured on 11.6.6 — five Studio Pro-authored drop-down filters store it empty, the shipped shape passesmx checkat 0 errors, and a filter written WITH it still fails CE0642 "Property 'Datasource to Filter' is required", because mxbuild resolves the property from the parent rather than reading what is stored.linkedDsis the only linked datasource among the eight multi-datasource widget packages intestdata/expr-checker.describe pageemits the named keys back, so describe → exec keeps each binding on the mapping it came from. A widget with ONE configured datasource keeps the genericDataSource:clause it has always been described with, and an unset second datasource drops out of the count — so the output of every widget shipped today is unchanged. A source whose schema key cannot be resolved falls back to the generic spelling rather than being dropped (#956's silent loss, reached by a new route). The detection runs even when a per-widget extractor already found one, because those know about a single datasource each: the drop-down filter's readsrefOptionsand neverlinkedDs, so a filter carrying both described back as single-source. -
notify workflow … target Module.Workflow.Name(docs/11-proposals/PROPOSAL_workflow_studio_pro_constructs.md, phase 4b) — names the element a notify reaches: a notification-started event sub-process, a notification activity, a notification boundary event or a wait for notification. The statement names the element and mxcli looks up what it is, because Mendix stores a different target type for each (captured from ako/TestApp, whose saved notify microflow is the reader's fixture). An element no notification can reach — a timer-started sub-process, a user task, a timer boundary event — is refused with what it is and the names that would work. Before Mendix 11.7 the action names its wait for notification directly (Activity), and mxcli writes that form there. Describe emits the clause, and the action is now writable over MCP. -
Workflow event sub-processes and notification events (docs/11-proposals/PROPOSAL_workflow_studio_pro_constructs.md, phase 4) —
event subprocess <name> on [non] interrupting notification|timer … { … };after the main body,notification <name>;, andboundary event [non] interrupting notification <name> { … }. Until nowdescribe workflowleft event sub-processes out, printed a notification activity as a comment and dropped notification boundary events, so a workflow with any of them could only be edited in Studio Pro. The shapes come from ako/TestApp, built in Studio Pro 11.14 and saved; the saved document is the reader's fixture, which matters because gen has no type for the timer starts, the notification activity or the notification boundary events. Measured on mxbuild 11.13.0: a sub-process body with no end is CE0105, so mxcli appends the End as it does for the main flow; a jump into or out of a sub-process is CE6682 (MDL-WF05, which now checks each flow as its own scope); a timer start without its expression is CE0126 (MDL-WF14); two interrupting boundary events on one activity are CE6697 (MDL-WF15); a start event named like an activity is CE0495, so names are made unique across the flows. The version gates were measured by loading each construct alone into a blank project: notification-started sub-processes load from 11.10 (11.8 per gen), the notification activity and boundary events from 11.11, timer-started sub-processes only from 11.13. A rewrite must restate them;alter workflow … insert boundary eventrefuses the notification kinds for now. Over MCP they go into the constructor, and a rewrite replaces the stored sub-processes. -
Multi-user task completion rules:
participants,decide by,await all users(docs/11-proposals/PROPOSAL_workflow_studio_pro_constructs.md, phase 3) — how many participants must respond (all, a number, a percentage), how their outcomes decide the task (consensus,majority more than half,majority most chosen,threshold <n> percent|votes, each withfallback '<outcome>';veto '<outcome>';microflow M), and whether it waits for everyone. The shapes were captured from Studio Pro rather than read off gen: the examples were built in Studio Pro 11.14 over MCP, saved to ako/TestApp, and the saved document is now the reader's fixture — which is how "more than half" is known to be an Absolute majo...
v0.22.0
[0.22.0] - 2026-09-14
Headline: The gap between a model that validates and an app that works. Almost everything fixed here was invisible to mxcli check, to mx check and to mxbuild — a workflow that loads, validates and then deadlocks at run time; a describe round trip that deletes what it just described; a widget spelling that storage drops — because in each case the model Mendix loaded was perfectly valid, just not the one that was written. Alongside that, offline synchronization, SOAP request bodies and a microflow's join points become authorable, closing three of the places where MDL could describe a model it could not write.
Added
-
CATALOG.GRAPH_MODULE_CYCLES— the modules that depend on each other (#1060), plus amodule_cycles()Starlark builtin for enforcing a no-circular-modules policy. Not a rollup ofGRAPH_CYCLES: modules reference each other through documents that need form no cycle themselves, which is the ordinary shape and why the document-level table is legitimately empty for a genuinely tangled pair. Computed over every reference kind, matchingGRAPH_MODULE_COUPLING— the table it is read beside — rather than the structural subset the clustering uses; on a blank Mendix appAdministration → Atlas_Coreis alayoutedge and nothing else, so a module-cycle table on the structural subset would answer "none" for exactly the pair that gets reported. Each row carriesRefKinds: the kinds on that module's edges into the rest of the cycle, not everything it points at. -
CATALOG.GRAPH_ANALYSIS_SCOPE— which reference kinds reach the computed graph. The two graph families disagree by construction and there was no way to see it from SQL:GRAPH_MODULE_COUPLINGand friends read every kind straight offrefs, while the computed pass (communities, cycles, layers, centrality) is restricted to the structural kinds. On a stock 11.14 app that is 110 of 316 edges. One row per kind with its edge count and whether it is in the asset graph; theINlist is generated from the filter itself, so the view cannot drift from what it describes. -
Icon: image <qn>andIcon: glyph <n>on page widgets (#1059) — Mendix stores three icon elements on a button and MDL could spell one, so the other two were readable and unwritable. DESCRIBE learned to flag them rather than destroy them (see Fixed, below); this makes them authorable, so the round trip is lossless rather than merely honest.actionbutton btnEdit (Caption: 'Edit', Action: nothing, Icon: 'Atlas_Core.Atlas_Filled.pencil') actionbutton btnLogo (Caption: 'Logo', Action: nothing, Icon: image MyModule.Images.logo) actionbutton btnHome (Caption: 'Home', Action: nothing, Icon: glyph 57377)
The bare form stays the icon-collection icon, so no existing script changes meaning; a name may also be written unquoted, as every other reference into the model is. The keyword is the whole point: the first two forms are spelled identically and point into different documents, so nothing else can separate them. Same vocabulary and same ordering trap as navigation's
icon image/icon glyph, and the shared resolver (types.MenuIconKind) rather than a second copy —pages.Iconcarried its own three-value enum for the same three things, which is precisely the drift that produced the bug.mxcli check --referencesnow resolves each kind against its own collection, which it could not do before: an image reference was checked against the icon collections and reported as a typo. When the kind is wrong it names the remedy — "MyFirstModule.ImagesIS an image collection — writeIcon: image MyFirstModule.Images.workflow" — rather than leaving a correct name looking misspelt. MDL078 (undefined glyph code) now covers widget glyphs too: it read the statements through its own walk, so the day a widget could carry a glyph every one of them would have passed in silence; both it and the reference check now consume the package's single icon walk.The legacy engine wrote
Icon: nilon every action button unconditionally — a button icon has been authorable since #602 and only the modelsdk engine ever wrote one, so under--engine legacyit was dropped on every write, silently, since a null Icon is what an iconless button stores. Both engines now write all three.Measured on 11.13.0 against a real project: all three authored,
mx check0 errors,mxbuild --target=deployBUILD SUCCEEDED, and the three elements on disk with the right$Typeand payload. The controls are what pin it — the same page withimagedropped produces exactly[error] [CE1613] "The selected custom icon 'MyFirstModule.Images.workflow' no longer exists." at Action button 'btnImage', and a widgetIcon: glyph 57562fails the deploy build withSystem.InvalidOperationException: Sequence contains no matching element, naming the page (so MDL078's message no longer promises "layout"). Corpus sweep clean at 531 scripts. -
DESCRIBE MICROFLOWlearns the graphs that nesting cannot express (#923) — a Mendix microflow is an arbitrary digraph and nestedif/then/elseis single-entry/single-exit, so anything that rejoins outside that shape was described as something it is not. Three parts, shipped in order, sharing one vocabulary.-
merge <label>/join <label>— naming a microflow's join points — MDL could not say where an error path rejoins the normal one. The only two spellings were "terminate on your own" and an emptyon error { }, which means "fall through to the enclosing branch's continuation", so every other rejoin collapsed to that one target on adescribe→execround trip, silently.Measured: repointing
FeedbackModule.SUB_Feedback_SendToServer's error edge from the tail merge to the merge before the AppId split — "on error, return empty" becoming "on error, re-enter the split" — gave two behaviourally different graphs that DESCRIBE rendered as the same MDL, bar one layout annotation, with no warning; executing that MDL reproduced the tail-merge graph in both cases.mxcli check, Studio Pro and mxbuild were clean throughout, so nothing could have caught it.The label is MDL-only — a Mendix merge stores no name — so it is resolved at build time and at describe time and never written to the model. Forward and backward references both resolve, which is what makes a retry loop expressible. It also covers crossed branches on the authoring side: an inner split's branch landing where an outer split's branch lands, which no nesting of
ifreproduces. -
Mode 2 — the default rendering is faithful for crossed branches. A merge reached by branches of two different splits has no place in a tree, so DESCRIBE walked past it and emitted the conjunction along one path: the reporter's activity ran on
not($A) or $Band described as$A and $B. Every branch now ends injoin <label>and the shared part follows as amerge <label>section, so MDL-FLOW01's "this description is NOT equivalent and must not be re-executed" is retired for the recombinable class — that sentence was the point, since it sent people to Studio Pro for an edit they could have made in MDL. Three traps behind it, each of which produced plausible output: a crossed merge is emitted the opposite way round from an error rejoin (joined by each branch, declared once afterwards, so the label alone cannot drive it); the join has to be emitted before the visited guard, since arriving twice is what crossed means; and MDL-FLOW01 is retired on what was emitted, not on what was labelled — an inheritance split walks through a labelled merge without emitting a join, and suppressing on the label alone silenced the warning on a description that is byte-identical with and without the change. -
DESCRIBE MICROFLOW … NORMALIZED— a third description mode, for the graphs nesting cannot express (#923). Nestedif/then/elseis single-entry/single-exit; a Mendix microflow is an arbitrary digraph. When an inner split's branch lands where an outer split's branch lands, the default describer walks the graph as a tree and emits the conjunction along one path: the reporter's activity ran onnot(c1) or c2and was described asc1 and c2— with their expressions, a program that always logged described as one that never did, withmx checkclean throughout.NORMALIZEDfolds the branch guards into one condition instead, which is equivalent and duplicates nothing. On the reporter's graph it emitsif $B or not($A) thenwhere the default emitsif $A then if $B then.Not a second describer.
normalizeCollectionrewrites a copy of the graph into the equivalent properly-nested one and hands that to the describer that already exists, so Mode 3 inherits every activity renderer, annotation and layout rule and cannot drift from Mode 1. The folded split keeps its$ID, position and annotations; its stored Caption is cleared, because it labels the original guard and leaving it puts a second, wrong condition in the more prominent place. The entry merge is spliced out: after folding it has in-degree 1, whichdescribe→execwould delete anyway, so removing it here makes Mode 3's output describe exactly the graph its own re-execution builds.
-
-
A view entity's association to a persistent entity is authorable (ako/view-entity-examples FINDINGS §1) — selecting a persistent entity's id under an alias (
select m.ID as MeterRef) gives the view entity an association named after the alias. The id column is not one of the view entity's attributes, and there is no separate declaration anywhere: Studio Pro creates the member when the column is added, so the column is the declaration.mxcli modelled the select list as attributes only, and got three things wrong at once.
checkaligns columns with declared attribute...
v0.21.0
[0.21.0] - 2026-09-06
Headline: Widgets become first-class MDL rather than a second dialect. Every other extension point resolves against the project; a widget resolved against a hardcoded list in the grammar, was named and described differently, and was invisible to the reference graph. Its own definition now drives all four — the grammar, the checker, the catalog and DESCRIBE — and the cost of the old arrangement was not theoretical: a team that could not express the HTML Element's attributes list fell back to tagContentHTML, which executes same-origin, precisely the risk the sandbox they wanted exists to prevent (#1036). Alongside it, message definition collections become authorable, closing the last of a mapping's four schema sources, and mxcli brain gives a project somewhere to keep what mxcli cannot compute. Running under all of it is one theme: a statement that reported success now has to have survived the round trip. A long list of things mxcli wrote, reported clean, and then destroyed on the next rewrite — an unstated doc comment across all 29 document types, a Reduce's fold, a SOAP-sourced mapping, a menu icon, a hand-placed parameter, a translation — and several that reached nowhere a build could catch them: PREVENT produced an app whose runtime would not start, duplicate element $IDs made a project Studio Pro could not open, update security was inert on every MPR v2 project, and SIGN_OUT rendered a button that did nothing, with mxcli check, exec and mx check all clean.
Added
-
Widget definitions drive the grammar, the checker, the catalog and
DESCRIBE(#1036, PROPOSAL_def_driven_widget_bodies.md) —mxcli widget initgenerates widget documentation for agents to follow, and fed back intomxcli checkverbatim the generatedhtmlelement.mdfailed on the first line of its own example. The doc generator derived a keyword for every object list and child slot in a definition while the grammar accepted nine hardcoded ones — two lists, nothing comparing them. 16 of 46 documented constructs parsed; it is 50 of 50 now.DESCRIBE WIDGETanswers what a widget is in MDL's own language —describe widget combobox, or by package id — reporting body containers, which properties are authorable, and an example that parses as written, with the bindings narrowed by the widget's own rules and properties its configuration hides left out.- The validator has its own notion of a widget (slice 0). The grammar was the widget-kind validator: an allow-list, so an unknown kind could not parse and the checker never grew an independent idea of one. Two mistakes therefore passed
checkand failed atexec— MDL-WIDGET25 (an id that resolves to nothing) and MDL-WIDGET26 (a container keyword used where it is not a body), the second actively suppressed by a helper that treated a container keyword as always-an-item wherever it appeared. Properties are now read from the widget's.mpkrather than a hand-written list, and a documented alias is accepted alongside the schema key. - A widget is a reference target (slice 5) — a pluggable widget was the one MDL extension point with no edge in
CATALOG.REFS, so "which pages use this widget?" was unanswerable while the same question about a Java action was one query. It is the question an upgrade asks: a.mpkno page uses is dead weight, one used on forty pages is not something to swap lightly. Widgets inside an object-list item are indexed too. DESCRIBEstopped losing widget bodies — it now emits the widget's own name, reconstructs a pluggable widget's containers instead of dropping them, and emits property keys verbatim rather than PascalCased.
-
DESCRIBE WIDGET— a widget's definition, in-language —describe widget combobox;ordescribe widget 'com.mendix.widget.web.htmlelement.HTMLElement';reports each property's key, type, caption, category, required flag, default and enumeration values, plus the dynamic rules the widget's editor uses to hide properties under some configurations (the ones that cause CE0463 when written into the pruned half).A widget was the only MDL extension point without one: a microflow, nanoflow, Java action and JavaScript action all describe in-language against the live project. That gap is why
mxcli widget initgenerates markdown documentation at all — and why that documentation could drift from what the parser accepts, as reported in #1036. The statement andmxcli widget describeare now the same function, so they cannot disagree.It also reports the widget's body containers — child slots and object lists, with each object list's item properties — and marks which are authorable from MDL today, since most are not (30 of 46 across a stock project). That answer is derived by parsing a probe, never from a list: the defect behind #1036 was two keyword lists with nothing comparing them, and a third list here would repeat it one layer up. It also means the marks correct themselves when the grammar catches up.
It emits an MDL example that parses as written. The head form (
gallery widget1vspluggablewidget '<id>' widget1) and every container in it are chosen by probing the real parser, and whatever the grammar cannot yet express is left out and named — including required properties that need a real entity or microflow from your project. This is the half of the generated.mdthat was wrong: its example failed on its own first line. Because both halves are derived rather than written down, the example widens on its own as the grammar gains ground.Bindings the example cannot fill in — datasource, attribute, action, expression, selection — are named rather than invented, and narrowed by the widget's own visibility rules: a property is required only where the editor shows it, so Combo box's eleven drop to six under its default configuration (and would drop further;
16 of 32 editor hide-rules recognizedis the current ceiling). Pruning is conservative by design — an indeterminable condition never prunes, and a rule about an object-list item never prunes the widget's own property.It works with no project open, answering from mxcli's embedded set — "what can I write here?" is asked before anything is open. With a project the answer is better: the installed
.mpkis version-accurate and is the only place a Marketplace widget appears. -
Message definition collections are authorable (ako#272) —
CREATE [OR MODIFY] MESSAGE DEFINITION COLLECTION, plusALTER … ADD|DROP|RENAME DEFINITION,SHOW,DESCRIBEandDROP. A message definition is the source for 74 of the 327 mappings in the demo corpus, and was the last of a mapping's four schema sources a script could not create. Unlike an XML schema or a WSDL it holds nothing external: every element names an entity, an attribute or an association, which is why it is derived rather than parsed.Nearly the whole document is derived, and the oracle for that is a hand-authored Studio Pro collection (ako/TestApp) rather than the corpus — round-tripping it against the stored bytes found three derivations wrong that a census of shipped modules had not shown. The sharpest: a node's
Pathis a chain of original names, and an association contributes two segments — its own, then the target entity's — confirmed afterwards at 4,707 of 4,707 elements across TestApp and the nine demo apps.MaxOccurson an association follows the direction of traversal, not the association's type, so an association connecting the two entities in neither direction is refused rather than defaulted: a wrong cardinality exposes a list as a single object.describe→execnow reportsUnchanged, meaning the rebuild was semantically equal to Studio Pro's own document and the write was elided. -
A project knowledge store:
mxcli brain— an opt-in store indocs/brain/for what mxcli cannot compute: why a pattern was chosen here, which marketplace version broke what, what a recurring mxbuild error means in this app. The governing rule is that anything derivable from the model is answered by a command and never written down, because a note that transcribes the model disagrees with it silently and nothing checks prose. An agentcaptures to a git-ignored queue and a personpromotes;mxcli lintprints the unpromoted count, since a report onlybrain checkprints is a report nothing demands.- Records shard by anchor scope, so a session loads
project.mdplus the modules it is touching rather than the whole store. An entry's first anchor names its file (@Sales.Order→modules/Sales.md) and there is no index to maintain, because the module prefix is the file name. - Requirements and slices, with progress derived from the model — a requirement's anchor inverts a decision's. A decision's points backward at what exists, so one that stops resolving means the decision is stale; a requirement's points forward, so one that does not resolve means not built yet. That is not a stylistic distinction: filed as an ordinary entry, one unbuilt requirement takes
brain checkto exit 1, which is why it is a separate kind. It also makesbrain plana real progress report — creating the microflow a requirement names moves the count with the plan file untouched. - Open questions, whose anchors are deliberately not checked: a question routinely names something that does not exist, because the question is often precisely whether it should.
bootstrap-appasks for requirements at the interview and records them by default.
- Records shard by anchor scope, so a session loads
-
SQL referential actions on an association, and the message
PREVENTneeds — an association written withDELETE_BEHAVIOR PREVENTproduced an app whose runtime would not start, and not as a build error:mx checkreports 0 errors either way, and t...
v0.20.0
[0.20.0] - 2026-08-28
Headline: Mendix 11.14 and Java 25, and the layer wrapped around a page — layouts, navigation profiles, mappings, translations and themes — becomes something a script can write. A blank 11.14 project now builds, boots and runs as generated. CREATE LAYOUT closes the last document a page depends on that MDL could not write, so an app no longer has to start on an Atlas layout it cannot touch; a census of 327 real mapping documents turned the import/export mapping surface from a guess into a list and this release works through it; translations survive a rewrite and can be authored in bulk; and a project can carry, scaffold and switch between its own themes. Running underneath all of it is one theme: a statement that reported success now has to have meant something. Several of the fixes below are round-trips where describe printed a document mxcli could not express as one it could, and that output parsed — so the model was rebuilt, valid, and different.
Added
-
Import and export mappings, closed against every mapping in Mendix's own demo apps (#248, #260, #262–#268) — a census of the 327 mapping documents in the demo apps turned the mapping surface from a guess into a list, and this release works through it. The gaps were not exotic: the largest was an array-rooted structure, in 122 of the 327. Worse than any missing syntax was what
describedid with one — a mapping mxcli could not express was described as one it could, and that output parsed and executed, so adescribe→execround-trip silently rebuilt a different mapping (#260). Every shape below is pinned against Studio Pro documents rather than inferred.- A structure decides where its own root is (#248) — both executors looked the root up at
(Object). An array-rooted JSON structure builds its root at(Array)and its item at(Array)|(Object), so the root resolved to nothing and every member was then reported missing "at (Object)" — a node that structure never had, which reads as a wrong key rather than a root looked up in the wrong place. No new syntax: Studio Pro does not ask either. - Mappings over a message definition (#263) — the third source form beside
with json structureandwith xml schema, and the second-largest gap at 74 of the 327. A message definition is derived from the domain model rather than from a payload sample, and carries two names per node — its own and the singular one its items take — so a mapping over it stores both path families and gets its own builder.describeused to drop the source clause and still parse, so 70 of those 74 were in the silent-loss class. - Custom object handling —
find X by <microflow>(…)(#264) —ObjectHandlinghas four values and MDL spelled three. Describing one of the 56 documents that resolve an object by calling a microflow dropped the handler, and the output then executed into a mapping that creates objects where the original called a microflow.byreads as a modifier onfind, which is what Custom means. - Export arrays and entity-less grouping nodes (#262) — mxcli put the entity and association on both levels of an export array, so the author declared them twice and the result matched no Studio Pro document; the container is now generated bare, as it is in 93 of the 93 real array containers, and
describeunwraps it so the single declaration round-trips. Separately, a JSON object with no Mendix object behind it is nowgroup as key { … }. A value directly under a group is refused with Mendix's own reason — CE0061 "No entity selected" — rather than left for the build to report. - Root a mapping at a nested element —
root a/b/c(#267) — a mapping's root need not be the schema's root; 13 documents start lower. The path may pass through an array, where a value reference may not, in which case the mapping is rooted at that array's item, which is what Studio Pro stores.describeused to print such a mapping as if it were rooted at the document root, and that output parsed and produced members that do not exist at that level. - Arrays of primitives (#268) —
"tags": ["a","b"]is one entity per string, modelled by Mendix as a Wrapper element whose single child binds the(Value)marker. 34 documents contain one and MDL could express none;describeleaked the markers, which do not parse. The JSON STRUCTURE builder was writing unresolvable paths for a primitive array as well, so the mapping was unreachable rather than merely awkward. - An import mapping declares its input object, and a
findsays what it should do when nothing matches instead of taking one behaviour by default.
- A structure decides where its own root is (#248) — both executors looked the root up at
-
Bulk translation:
DESCRIBE TRANSLATIONSandCREATE [OR MODIFY|REPLACE] TRANSLATIONS(PROPOSAL_translations.md slices 2–4) — every user-visible string of a project, as one MDL file per language:create translations for nl_NL ( 'Save' as 'Opslaan', … ). Entries useasrather than a colon because a translation maps a user-provided name to another name. Measured on a real project, a whole app is 411 distinct source strings, so one file per language is comfortably practical, and keying on the source string meansSaveis translated once for all ~40 places it occurs.The three verbs take the language as the thing that exists: bare
CREATErefuses when it already has translations (the "add a language" statement),OR MODIFYmerges, andOR REPLACEmakes the file authoritative — a translation whose source the file does not name is removed, and the run names what it deleted.IN <Module>scopes both directions and, underOR REPLACE, bounds the deletion, without which per-module files would wipe each other on every run.DESCRIBEemits theCREATEform, and an untranslated string comes back with an empty target — so the auto-translate loop needs no separate export format: describe, fill in the right-hand side, execute. A dictionary key that matches nothing is reported rather than skipped, because a source edited after the file was written stops matching; where the translation identifies the moved source unambiguously the run names it, and where it does not, it says so instead of guessing.Writes patch the stored BSON directly rather than going through the document rebuild path, so a translation can be written into any document type — including ones mxcli cannot otherwise round-trip — and re-running a file writes nothing.
-
Layouts are authorable:
CREATE LAYOUT— the last document a page depends on that MDL could not write, which is why the topbar was out of reach. A layout is nowcreate [or replace] layout Module.Name (layouttype: 'Responsive') { scrollcontainer … }, with four element types pages did not need:scrollcontainer,region top|right|bottom|left|center,placeholder, andnavigationtree. Verified in a browser on a real 11.13 app — all three regions render in place and the page's own widget lands in theMainplaceholder — not just against a cleanmx check, which a layout can pass while rendering wrong.DESCRIBE LAYOUTnow emits re-executable MDL rather than a commented tree, which is what makes the documented workflow work: describe an Atlas layout, change the qualified name, run it. That is the copy operation, so there is noCOPY DOCUMENTverb and none is needed. Writing intoAtlas_Coreis refused — Mendix's own guidance is not to edit the supplied layouts, because a Marketplace update replaces the module and the edit is gone. Making that guard real also meant fixing a quieter bug:FromAppStorewas only populated byListModules, so a lookup by name readfalsefor every module and any guard on it would have been inert.The document is pinned to the ten keys Studio Pro writes, measured identical across all 22 layouts Atlas ships on 11.13.0.
modelsdk/genoffers a whole family of placeholder properties onLayout—MainPlaceholderNameand six more — thatgenerated/metamodeldoes not declare and no real layout carries; writing one gives a layout mxbuild accepts at 0 errors and Studio Pro cannot open, since it resolves every stored property against the type's list. Which placeholder is "main" is a naming convention instead (22 of 22 name oneMain, and a page binds by qualified name anyway), solayouttypeis the only header property and anything else is an error rather than an ignored key. The platform is inferred from the layout type — the web and native vocabularies are disjoint — so there is nonative:flag to contradict it. Authoring is modelsdk-only; the legacy writer, whose layout serializer emitted a string$ID, noContentwrapper and aLayoutTypeon the wrong node, now refuses instead. -
Layouts are editable and pages can be moved onto them:
ALTER LAYOUT,ALTER PAGES … SET LAYOUT— authoring a layout was half the job; a layout no page uses changes nothing, and a rewrite is the wrong tool for changing one you did not write.ALTER PAGES [IN <module>] SET LAYOUT = X [WHERE LAYOUT = Y]is the migration form — an app has one layout and many pages, so moving off Atlas_Default is one statement rather than forty. Pages in Marketplace modules are skipped and named rather than refused (a project-wide repoint that stopped dead on Administration's pages would be unusable), and aWHERE LAYOUTnaming a layout that does not exist is an error, not a "0 pages" success for a typo.Both forms now refuse a repoint that would leave a page bound to a placeholder the target layout does not declare, and name
map (Old as New)as the remedy. Without the check the rewrite producedNewLayout.HeaderLeftfor a layout with no HeaderLeft; mxbuild does catch that (CE1613, measured on 11.13) but at the far end of a build, naming the page rather than the statement — and it matters more in bulk, where one page out of forty using an extra placeholder is exactly what nobody chec...
v0.19.0
[0.19.0] - 2026-08-21
Headline: A statement mxcli accepts is now a statement mxcli honours. This release closes a long list of clauses, annotations and document properties that parsed cleanly, reported success and were then dropped on the floor: a test's @setup and @verify, DELETE_BEHAVIOR PREVENT, a List View's specialization templates, a REST call's file-document response, a widget's contentparams, a workflow's boundary events, a decision's rule call. Alongside that, rules become the last microflow-family document type mxcli can both read and write, mxcli check --references type-checks expressions against the catalog, and skills can now ship assets — Java actions, Vega specs, MDL — rather than prose alone.
Added
-
Rules are an authorable document type —
LIST RULES,DESCRIBE RULE,CREATE RULE,DROP RULEandMOVE, on top of a rule finally being readable. AMicroflows$Rulewas the one document type mxcli could reference but not read:IsRuleanswered a yes/no question about a name, and nothing could list a rule, show its signature or render its body, so a rule was invisible to every surface except the decision that called it. A rule is handled the way a nanoflow is — its own semantic type and its own listing, soSHOW MICROFLOWSstill lists microflows only.- A rule is a catalog object.
CATALOG.RULESsits besideCATALOG.NANOFLOWS, with aRULErow inCATALOG.OBJECTSand rule bodies indexed intoCATALOG.SOURCE. The reference builder walks a rule's body, which had to land in the same change: without it, a microflow called only from inside a rule was reported dead byshow callersandGRAPH_DEAD_ASSETS— worse than not knowing about rules at all. - What a rule may not contain is refused at check time, by the same
validateRulethe executor calls, socheckandexeccannot disagree. The restrictions are measured rather than taken from the documentation: on mxbuild 11.13, a create action inside a rule is CE0009, and a void or String return is CE0103 + CE0139. - Authoring is modelsdk-only; the legacy engine refuses, as it does for menus. An mxcli-authored rule carries exactly Studio Pro's twelve keys —
ExportLevel: "Hidden"and a bareFlowsmarker included, both of whichmx checkwas happy to do without.
- A rule is a catalog object.
-
mxcli check --referencestype-checks expressions — newmdl/exprcheckandmdl/exprcatalogpackages.if $obj/Status = 'Open'— the shape people actually write — passed a check that caught the same mistake written as a create or change member. Two causes, both invisible:$obj/Attrtyped to nothing, so every rule downstream of it stayed quiet; and even resolved, the rule keyed off an assignment slot, which a comparison does not have. Attribute paths now resolve to a real entity through a newEntityScopeseam, and a multi-hop path resolves each association through the catalog's association index — an expression path, unlike an XPath one, does not spell its intermediate entity, which is the whole reason it needs a resolver. The same defect gets the same code and message wherever it is spotted, and nanoflows are checked too. Only anerrorseverity fails the run, and the false-positive probe that gated that — all 21 microflows of a Mendix 11.13 app, most of them marketplace-authored and Studio Pro-validated, described back to MDL and re-checked — came back with 0 findings. -
Skill packs — a skill that carries assets, not just prose —
mxcli skill list|add|remove|upgrade. mxcli ships 65 skills and every one was a single Markdown file, which was never a design decision so much as the limit of the mechanism: the embed wasskills/*.md, the sync was flat, and the write path joined onfilepath.Base, so a nested pack was silently flattened —references/install.mdandspecs/install.mdcollided and one vanished with no error. Three packs are vendored:mendix-vega-charts(Vega-Lite spec templates plus a headless checker),mendix-odata-pushdown(Java actions applied through MDL, with$selectpushed into the source query) andmendix-bulk-oql-dml. Packs are opt-in and never touch the model —skill addwrites files and prints the MDL command for the user to run deliberately — and installing one prunes assets a newer version dropped, because a stale spec template is worse than a missing one. -
A
FOLDERclause onCREATE, for every document type (#932) — import and export mappings had none, and neither did queues, scheduled events, regular expressions, workflows, menus, image collections, Java and JavaScript actions, database connections, data transformers or the four AI-agent documents. All of them could only be created at the module root, andDESCRIBEemitted no folder either, so describing a document Studio Pro had filed away and replaying it recreated it unfiled. The clause goes in one place — straight after the qualified name — rather than adding a third per-doctype convention to the two that already disagree. It also closed a trap:createWorkflowStatementread DISPLAY / DESCRIPTION / DUE DATE by counting string literals in order, so a folder path silently became the display name and shifted every later clause by one. The tokens are now labelled in the grammar and read by label. -
MOVEaccepts every top-level document type (#932) — it listed nine; the other twenty-two were never unimplemented, merely unlisted, soMOVE IMPORT MAPPING …was a parse error whileMoveImportMappingsat fully wired on the interface, both engines and the mock, called from nowhere. The doctype list is now a grammar rule rather than an inline alternation, which retires the hand-written negation of every doctype keyword that toldMOVE FOLDERfrom a document move — a discriminator that could not have stayed in step with twenty-two more. -
List View specialization templates — a List View over a generalization can render a different body per specialization. MDL had no way to author one and, worse, no read path either. Measured on a Studio Pro-authored page carrying four templates:
describe→execleft 0 templates, withmx checkreporting 0 errors before and after — the document stays valid, it just holds less, which is why nothing downstream flagged it. A template is identified by the entity it renders rather than by a name (template for Pages.Bus { … }),ALTER PAGEcan reach into one, and their contents are indexed in the catalog. -
IN QUEUEon a call activity —CALL MICROFLOW Ops.ACT_Process(Order = $Order) IN QUEUE Ops.Work;, and the same onCALL JAVA ACTION. Running on a task queue is a property of the call activity, not a separate construct, and MDL could express it on neither — so a queue could be created and never bound to anything. The clause takes the same position on both, after the argument list and before anyON ERROR, andDESCRIBErenders it back. -
REST gains a binary request body and a file-document response
body binary $Doc/Contentson aREST CALL. Binary POST had been reported as impossible, and for the consumed operation it is —Rest$JsonBody,Rest$StringBodyandRest$ImplicitMappingBodyare the only three, which is why grepping the metamodel forRest$*Bodyappears to settle it. Mendix models a binary request body on the microflow activity instead (Microflows$BinaryRequestHandling). The earlier, correct-about-the-wrong-document conclusion stays as MDL-REST02, refusingbody: file— which had been sending the expression text.- Storing a response in a file document (#922) had no MDL form at all and both readers fell through to String, so
DESCRIBEreportedreturns Stringand the round trip wrote that back. Measured on mxbuild 11.6.6:ResultHandlingTypewentFileDocument→StringandVariableTypewentObjectType(MyModule.MyFile)→StringType, withmx checkunchanged — the activity silently retyped, with a valid model and a green build.
-
A published OData service can be published as GraphQL — one boolean over the same resources. Every layer already had it except the one an author reaches, and since an unknown OData property is an error (MDL-ODATA01), a script asking for GraphQL was rejected outright.
create or modifythat does not mention it cannot turn GraphQL off on a service that has it, and an omitted value is left alone rather than opted in. Gated at Mendix 10.14, where it was introduced: writing a property a version's metamodel does not have is not a build error, it is a document Studio Pro cannot open. -
ALTER PROJECT SECURITY GUEST ACCESS ON|OFF, andON ROLE <UserRole>— anonymous access was readable but not writable, so an app with a public area could not be built unattended: every headless run ended with a manual step in Studio Pro. Wired the wayDEMO USERS ON|OFFalready was — same AST node, same unit, same write choke point, both engines. -
@setupand@verifydo what they say inmxcli test— both were parsed into the test case and read by nothing.@setup Mod.Seednames a microflow called before the test's own statements. It is repeatable, and a file's header comment may declare one for every test in the file — which is what makes it worth more thancall microflow X;at the top of each body, and is only readable because a header comment stopped being swallowed by the first test (#927). A header may carry nothing else:@expect,@verify,@throwsand@cleanupdescribe one test's execution and are refused there by name rather than ignored.@verifyruns its OQL against the app after the microflow returns, keeping three outcomes apart: holds, does not hold (FAIL, with the value that came back), and cannot be evaluated (ERROR, counted with the failures). Previously no OQL — well-formed or not, true or not, against a real entity or an invented one — could make a@verifyfail.@cleanup rollbackis refused alongside it, s...