Skip to content

Releases: mendixlabs/mxcli

Nightly 2026-10-20

Nightly 2026-10-20 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 20 Oct 17:03
3b24e5a

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

Choose a tag to compare

@github-actions github-actions released this 15 Oct 13:52

[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-match turns 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: search exited 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; the SEARCH statement 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 a System.FileDocument specialization. Before, there was no way to author one: pluggablewidget 'Forms$FileManager' had no definition, the bare keyword was refused as MDL-WIDGET25, and describe page printed 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 … set changes all four properties. A file manager in no data container is MDL-WIDGET45 at check, and one in a data view on an entity that is not a FileDocument is refused by exec — both were mxbuild's CE6810 after a clean exec.
  • 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 by describe, had no MDL spelling, was written empty and was dropped by alter … 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's Visible: (#1461) are now written too; describe → exec used to make a Studio Pro row always visible.
  • += / -= for reference-set members inside change (#1473) — change $Obj (Name = 'x', Mod.Assoc += $Item): a Change object activity that mixes an Add or Remove with other members had no spelling, and describe printed the Add as =, which replaces the set.
  • ALTER PAGE addresses navigation list items and widgets inside pluggable widgets — nav item('Home'), nav item(t1) and nav item@2 name 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). set also writes a text box's Placeholder, Password and Validation (#414), a static image's Image (#1464), and a Label on an input that showed none (#292).
  • Image: $currentObject binds 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 extend System.Image.
  • describe module --all (#1435) — the describe module subcommand printed only the module and its roles although the help promised "all contents"; --all runs describe module … with all, and the help says what each form does. with all now 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 --local hard-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 @Position takes 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, and move maps a document's roles to same-named roles of the target module without creating any. Breaking for scripts that granted to <Module>.User without creating it: add create module role <Module>.User; — check --references and exec both 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.User still adopts such a role.
  • check -p resolves many more references before exec would refuse them (#1472) — each of these passed check and failed exec or 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 where names (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 is headerText), and a file manager outside a System.FileDocument data view (CE6810);
    • a change or member path over a list retrieve (MDL-RETRIEVE03, #1422), show message … type ok (MDL-SHOWMSG01), import from mapping without a result entity, which made the project unloadable (#1418), and alter settings JavaVersion on a Mendix 9 model, which stores none (#1438).
  • A multi-file check ends 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 -p name the project they auto-detected (#1444) — the notice gives the absolute path and how to choose another, and with several .mpr files in the directory it lists them and asks for -p instead of only saying -p is required.
  • Studio Pro document names in describe --help and the catalog (#1395) — consumedrestservice, publishedodataservice, taskqueue, aimodel, appsecurity, …, and catalog views consumed_rest_services, published_odata_services, task_queues, …. Every old keyword and view name still works.
  • mxcli syntax resolves statement verbs and stops teaching deprecated spellings (#1389) — mxcli syntax grant (and revoke, refresh) found no topic, and several examples used spellings check flags as MDL-DEPRnnn.
  • One-shot mxbuild runs have a timeout and a stall watchdog — mxcli new and docker build could hang forever behind a Gradle build that stopped making progress. They now stop after MXCLI_MXBUILD_TIMEOUT (30 minutes) or MXCLI_MXBUILD_STALL_TIMEOUT of silence (10 minutes), printing the last output lines and the Java home used.
  • mxcli init titles the generated docs after the app, not the repository folder, and the bootstrap script defau...
Read more

v0.26.0

Choose a tag to compare

@github-actions github-actions released this 11 Oct 18:44

[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 snippet add and drop parameters (#1234) — add parameters $Order: M.Order and drop parameters $Order, beside add variables / drop variables. Before, add parameters was a parse error ("mismatched input 'parameters' expecting VARIABLES_KW") and the only route was create or replace page, which rebuilds every widget and loses what describe does not round-trip. The declaration is CREATE's Params: rule, written by the same converters (so the 11.5+ IsRequired/DefaultValue gate holds). A page with a Url must 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 the teamserver-git skill (#1337) — Studio Pro attaches a note under refs/notes/mx_metadata to 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 .mpr unit index and mprcontents at the commit and its parent — for the commits not yet pushed (or the ones named), keeps any note Studio Pro wrote (--force replaces 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 new teamserver-git skill 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), push refs/notes/mx_metadata with 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, and languages() failed to load with undefined: languages. It returns one language per enabled language, from the project settings, with code, is_default and check_completeness, so a per-language check (every page title translated into each enabled language) can filter strings() 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: none is "Requires authentication: No". The methods are stored in the order written, which is how Studio Pro stores the ones ticked, and describe prints them in the stored order (it omits none). Left out, create or modify and alter keep 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 return System.User and take only a System.HttpRequest and/or System.HttpResponse (Mendix: CE0334, CE0336, measured with mx check on 11.14.0); exec and check --references refuse anything else as MDL-REST04. Executing the describe output 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 a Microflows$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 microflow renders the activity instead of -- Unsupported action: Microflows$SendEmailAction, and the description re-executes to the same activity. check type-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.mpr loads each page in one headless browser and prints the verdict run --page-check prints (title, heading, rows, visible text, error banners, console errors) plus failed same-origin requests and the HTTP status, then OK n page(s) or FAIL 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 -p a 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; the test-app and verify-in-runtime skills and /mendix:test now route "check a page" to it.
  • mxcli run --local runs as a background service — --detach starts 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 status prints one line (URL, uptime, last build and how it was applied, a pending change; a dead pid is reported as stopped). mxcli run wait blocks 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), so mxcli exec x.mdl && mxcli run wait is one honest call; --ready waits for the boot. mxcli run stop shuts 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 restart stops 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 the nohup / until grep / pkill loops 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-report counts 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 ...
Read more

v0.25.0

Choose a tag to compare

@github-actions github-actions released this 05 Oct 09:26

[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's definition entry on its own), describe message definition, drop message definition, list message definitions [in M] and alter message definition M.Name add|drop|set member …. A mapping names the document as with message definition M.Name. A three-part M.Collection.Definition from an older script still resolves on a converted project and is stored as M.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 collections on 11.15 points to list message definitions. describe prints the document form, so describe → exec round-trips. Before this, 11.15 projects showed no message definitions, and a mapping over one failed mx check with CE0270. This is an addition, so mdl 1 scripts are unaffected.
  • check notes a locale-less formatDecimal in hand-built JSON (ako#982) — MDL-JSONNUM01 (info) for formatDecimal(x, '0.00') inside a + concatenation whose string literals contain { or ":, which writes 12,50 for a Dutch user. Use toString(round(x, 2)) or pass a hyphenated locale. A display string ('Total: ' + formatDecimal(…)) is not flagged.
  • mxcli fix hashes verifies and repairs the MPR v2 ContentsHash index (ako#972) — every Unit.ContentsHash in the .mpr is compared with base64(SHA-256) of its .mxunit, and mismatches, missing files and orphan files are reported; the command exits 1 when any remain. --repair rewrites the mismatched hashes from the files in one transaction, under the same Studio Pro open-project guard as every write. Restoring a unit with git checkout leaves the index stale and mx check does not notice; measured on a TestApp copy, an entity added by exec and then reverted with a byte-level restore is reported as one DomainModels$DomainModel mismatch, and after --repair the verify is clean and mx check reports 0 errors. exec and docker check print 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 --local and the new mxcli diag -p app.mpr project 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, under CI, or with MXCLI_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) — without in <Module>, create [or modify|or replace] translations reaches 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 on describe translations, which emits it) keeps the run, and an or replace deletion, 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).
  • check foresees 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 (or association 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 with run --local); check now 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/Attr path, and not on text such as 'Total (incl. VAT)', 'length (cm)' or 'Price in $'.
  • A commit reference kind in the catalog (ako#963) — refs has a commit edge from a flow to the entity it commits: a commit action, or a create / change that commits (Yes or YesWithoutEvents), the latter beside its create / change edge. The entity of a committed variable is resolved like change / delete, loop iterators included, so commit $Order inside loop $Order in $Orders now has a row. refs_to("M.Order") in a Starlark rule answers which flows commit an order. commit is not in the analysis graph and not a caller kind. The catalog schema version is bumped, so a cached catalog rebuilds.
  • total_activity_count on the Starlark microflow struct (ako#963) — the catalog's TotalActivityCount (loop bodies included, at any depth) for every flow microflows() yields: microflows, nanoflows and rules. activity_count keeps counting a loop as one activity.
  • The catalog's widgets table 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) and HasConfirmation. Starlark widgets() exposes them as parent_widget_id, depth, class_name, style, dynamic_classes, action_type, has_confirmation, plus page_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 init and every init --sync-skills write .ai-context/mxcli-tooling.json (version, build time, date; rewritten only when the version changes). Any command that opens the project with -p and a binary older than the stamp warns once on stderr, naming both versions and how to update; init --sync-skills from 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.sh checks it before choosing a binary: an older mxcli on PATH is not linked in (it downloads MXCLI_TAG instead), an older ./mxcli is replaced, and the download lands through a temporary file so a ./mxcli symlink 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 of mxcli init — so project notes outside the markers survive. A file written before the markers is left alone by the sync, with a note; run mxcli init once to adopt them.
  • mxcli init and mxcli new create mdlsource/ (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, as lint -m lints 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 ...
Read more

v0.24.0

Choose a tag to compare

@github-actions github-actions released this 24 Sep 05:51

[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 GUID while keeping the $ID is data loss with nothing to see: the model stays valid, mx check passes, the build succeeds, and DESCRIBE is 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's FreshGUIDFields.

  • 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 a session_start naming its argv (mdl/diaglog, wired at newLoggedExecutor, 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, plus check immediately followed by exec of 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 in Appearance.DesignProperties, so SET dead-ended on it and ALTER STYLING was 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: '…'), plus modify and remove, and they appear in DESCRIBE SETTINGS. They are project settings, peers of UserEntity and DefaultTaskParallelism, 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 — describe emitted 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 with Order_ShipTo and Order_BillTo both Order → 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() and rest_operations() builtins (the catalog built both tables, including rest_operations.Timeout, and neither was reachable — so "REST/web services should have a timeout" could not be written as a rule at all), and service_ref / action_ref on the activity, so a rule that finds a RestCallAction can now learn what it calls.

  • A run's repeated Unchanged reports 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-page widget 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. The overview-pages skill gains the two forms its type table did not cover: a dropdownfilter over an association (Association: + datasource: + CaptionAttribute:, verified mx check 0 errors and round-tripped through describe), FilterType: between for 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 @position are laid out to be read, not just to be valid (#1154). Geometry and connection sides only: no MDL syntax changes, a statement carrying @position is never moved, and describe → exec still reports Unchanged 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 and mx check at 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 if draws 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 in return is 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 case order 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. describe now 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 listview that does not say editable: true is written Editable: false, and the list view's read-only context wins over editable: Always on the input, including inside a nested data view. The BSON is valid, mx check is 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.editable defaults to false). mxcli check now warns MDL-WIDGET31 on a list view that will be written read-only while it holds an input not marked editable: Never, and names editable: true as the fix. A quoted editable: '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 --references and failed the build with CE7033 (#1064) — call microflow M.F() in queue M.Q where F returns Boolean said "Check passed!", and mx check then 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 --references for 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) and ARCH003 (entities without a business key) both skip an entity with if entity.entity_type != "PERSISTENT". The catalog stores PERSISTENT, but LintContext.Entities normalizes the kind to Persistent before a Starlark rule re...

Read more

v0.23.0

Choose a tag to compare

@github-actions github-actions released this 21 Sep 11:16

[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 .mpk that 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 own package.xml: a module goes through the transplant writer (storage format preserved, theme modules included), a widget lands in widgets/, anything else is refused rather than guessed at. --file and a content id are mutually exclusive, and --version is refused with --file because it selects a marketplace release.

    This closes the gap the command's own help used to point at: marketplace download could fetch a package to disk, after which the only routes were Studio Pro or mx 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; --file hands them one.

    A module installed from disk carries no marketplace version stamp, because it has no marketplace identity: PerformInstall skips StampMarketplaceVersion when the version id is empty, so update and diff report — 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-package from one Mendix 11.14.0 app, installed with --file into a fresh MPR v2 app named by a relative -p — 4 units copied, 6 bundled files installed, mprcontents/ intact, SHOW MODULES lists the module with an empty Source column, mx check reports 0 errors; a second run reports "already installed" and changes nothing. A Mendix 10.6.4 theme package is refused with mx's own message (the reference project is still built by mx at the project's version, so mx 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 generic datasource: clause, so a widget exposing several datasources can be given each of them. A mode gated on hasDataSource is selected by a named datasource too, and the new hasDataSource:<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 bare hasDataSource cannot 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's dataSource="…"), not mapping order, so a DatagridDropdownFilter's refCaption resolves against refOptions' entity and its attr against linkedDs'. Verified end to end on a real project: both datasources persisted and the two attributes stored as NamedDS.Order.Number and NamedDS.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 check and exec and 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 blocked exec entirely.

    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.DataSource carries widget.xml's dataSource="…" and the generated chart definitions already map staticDataSource and dynamicDataSource with every dependent's link — it simply was not read. A series configuring one datasource, which is every series in every real chart since dataSet selects 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's linkedDs), so a .def.json mapping 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 passes mx check at 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. linkedDs is the only linked datasource among the eight multi-datasource widget packages in testdata/expr-checker.

    describe page emits the named keys back, so describe → exec keeps each binding on the mapping it came from. A widget with ONE configured datasource keeps the generic DataSource: 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 reads refOptions and never linkedDs, 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>;, and boundary event [non] interrupting notification <name> { … }. Until now describe workflow left 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 event refuses 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 with fallback '<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...

Read more

v0.22.0

Choose a tag to compare

@github-actions github-actions released this 14 Sep 14:47

[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 a module_cycles() Starlark builtin for enforcing a no-circular-modules policy. Not a rollup of GRAPH_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, matching GRAPH_MODULE_COUPLING — the table it is read beside — rather than the structural subset the clustering uses; on a blank Mendix app Administration → Atlas_Core is a layout edge and nothing else, so a module-cycle table on the structural subset would answer "none" for exactly the pair that gets reported. Each row carries RefKinds: 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_COUPLING and friends read every kind straight off refs, 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; the IN list is generated from the filter itself, so the view cannot drift from what it describes.

  • Icon: image <qn> and Icon: 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.Icon carried its own three-value enum for the same three things, which is precisely the drift that produced the bug.

    mxcli check --references now 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.Images IS an image collection — write Icon: 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: nil on every action button unconditionally — a button icon has been authorable since #602 and only the modelsdk engine ever wrote one, so under --engine legacy it 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 check 0 errors, mxbuild --target=deploy BUILD SUCCEEDED, and the three elements on disk with the right $Type and payload. The controls are what pin it — the same page with image dropped produces exactly [error] [CE1613] "The selected custom icon 'MyFirstModule.Images.workflow' no longer exists." at Action button 'btnImage', and a widget Icon: glyph 57562 fails the deploy build with System.InvalidOperationException: Sequence contains no matching element, naming the page (so MDL078's message no longer promises "layout"). Corpus sweep clean at 531 scripts.

  • DESCRIBE MICROFLOW learns the graphs that nesting cannot express (#923) — a Mendix microflow is an arbitrary digraph and nested if/then/else is 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 empty on error { }, which means "fall through to the enclosing branch's continuation", so every other rejoin collapsed to that one target on a describe → exec round 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 if reproduces.

    • 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 $B and described as $A and $B. Every branch now ends in join <label> and the shared part follows as a merge <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). Nested if/then/else is 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 on not(c1) or c2 and was described as c1 and c2 — with their expressions, a program that always logged described as one that never did, with mx check clean throughout.

      NORMALIZED folds the branch guards into one condition instead, which is equivalent and duplicates nothing. On the reporter's graph it emits if $B or not($A) then where the default emits if $A then if $B then.

      Not a second describer. normalizeCollection rewrites 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, which describe → exec would 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. check aligns columns with declared attribute...

Read more

v0.21.0

Choose a tag to compare

@github-actions github-actions released this 06 Sep 15:40

[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 init generates widget documentation for agents to follow, and fed back into mxcli check verbatim the generated htmlelement.md failed 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 WIDGET answers 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 check and failed at exec — 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 .mpk rather 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 .mpk no 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.
    • DESCRIBE stopped 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; or describe 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 init generates markdown documentation at all — and why that documentation could drift from what the parser accepts, as reported in #1036. The statement and mxcli widget describe are 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 widget1 vs pluggablewidget '<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 .md that 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 recognized is 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 .mpk is version-accurate and is the only place a Marketplace widget appears.

  • Message definition collections are authorable (ako#272) — CREATE [OR MODIFY] MESSAGE DEFINITION COLLECTION, plus ALTER … ADD|DROP|RENAME DEFINITION, SHOW, DESCRIBE and DROP. 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 Path is 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. MaxOccurs on 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 → exec now reports Unchanged, 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 in docs/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 agent captures to a git-ignored queue and a person promotes; mxcli lint prints the unpromoted count, since a report only brain check prints is a report nothing demands.

    • Records shard by anchor scope, so a session loads project.md plus 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 check to exit 1, which is why it is a separate kind. It also makes brain plan a 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-app asks for requirements at the interview and records them by default.
  • SQL referential actions on an association, and the message PREVENT needs — an association written with DELETE_BEHAVIOR PREVENT produced an app whose runtime would not start, and not as a build error: mx check reports 0 errors either way, and t...

Read more

v0.20.0

Choose a tag to compare

@github-actions github-actions released this 28 Aug 13:28

[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 describe did with one — a mapping mxcli could not express was described as one it could, and that output parsed and executed, so a describe → exec round-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 structure and with 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. describe used 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) — ObjectHandling has 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. by reads as a modifier on find, 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 describe unwraps it so the single declaration round-trips. Separately, a JSON object with no Mendix object behind it is now group 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. describe used 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; describe leaked 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 find says what it should do when nothing matches instead of taking one behaviour by default.
  • Bulk translation: DESCRIBE TRANSLATIONS and CREATE [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 use as rather 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 means Save is translated once for all ~40 places it occurs.

    The three verbs take the language as the thing that exists: bare CREATE refuses when it already has translations (the "add a language" statement), OR MODIFY merges, and OR REPLACE makes 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, under OR REPLACE, bounds the deletion, without which per-module files would wipe each other on every run.

    DESCRIBE emits the CREATE form, 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 now create [or replace] layout Module.Name (layouttype: 'Responsive') { scrollcontainer … }, with four element types pages did not need: scrollcontainer, region top|right|bottom|left|center, placeholder, and navigationtree. Verified in a browser on a real 11.13 app — all three regions render in place and the page's own widget lands in the Main placeholder — not just against a clean mx check, which a layout can pass while rendering wrong.

    DESCRIBE LAYOUT now 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 no COPY DOCUMENT verb and none is needed. Writing into Atlas_Core is 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: FromAppStore was only populated by ListModules, so a lookup by name read false for 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/gen offers a whole family of placeholder properties on Layout — MainPlaceholderName and six more — that generated/metamodel does 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 one Main, and a page binds by qualified name anyway), so layouttype is 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 no native: flag to contradict it. Authoring is modelsdk-only; the legacy writer, whose layout serializer emitted a string $ID, no Content wrapper and a LayoutType on 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 a WHERE LAYOUT naming 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 produced NewLayout.HeaderLeft for 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...

Read more

v0.19.0

Choose a tag to compare

@github-actions github-actions released this 21 Aug 13:20

[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 RULE and MOVE, on top of a rule finally being readable. A Microflows$Rule was the one document type mxcli could reference but not read: IsRule answered 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, so SHOW MICROFLOWS still lists microflows only.

    • A rule is a catalog object. CATALOG.RULES sits beside CATALOG.NANOFLOWS, with a RULE row in CATALOG.OBJECTS and rule bodies indexed into CATALOG.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 by show callers and GRAPH_DEAD_ASSETS — worse than not knowing about rules at all.
    • What a rule may not contain is refused at check time, by the same validateRule the executor calls, so check and exec cannot 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 bare Flows marker included, both of which mx check was happy to do without.
  • mxcli check --references type-checks expressions — new mdl/exprcheck and mdl/exprcatalog packages. 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/Attr typed 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 new EntityScope seam, 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 an error severity 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 was skills/*.md, the sync was flat, and the write path joined on filepath.Base, so a nested pack was silently flattened — references/install.md and specs/install.md collided 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 $select pushed into the source query) and mendix-bulk-oql-dml. Packs are opt-in and never touch the model — skill add writes 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 FOLDER clause on CREATE, 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, and DESCRIBE emitted 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: createWorkflowStatement read 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.

  • MOVE accepts every top-level document type (#932) — it listed nine; the other twenty-two were never unimplemented, merely unlisted, so MOVE IMPORT MAPPING … was a parse error while MoveImportMapping sat 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 told MOVE FOLDER from 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 → exec left 0 templates, with mx check reporting 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 PAGE can reach into one, and their contents are indexed in the catalog.

  • IN QUEUE on a call activity — CALL MICROFLOW Ops.ACT_Process(Order = $Order) IN QUEUE Ops.Work;, and the same on CALL 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 any ON ERROR, and DESCRIBE renders it back.

  • REST gains a binary request body and a file-document response

    • body binary $Doc/Contents on a REST CALL. Binary POST had been reported as impossible, and for the consumed operation it is — Rest$JsonBody, Rest$StringBody and Rest$ImplicitMappingBody are the only three, which is why grepping the metamodel for Rest$*Body appears 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, refusing body: 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 DESCRIBE reported returns String and the round trip wrote that back. Measured on mxbuild 11.6.6: ResultHandlingType went FileDocument → String and VariableType went ObjectType(MyModule.MyFile) → StringType, with mx check unchanged — 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 modify that 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, and ON 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 way DEMO USERS ON|OFF already was — same AST node, same unit, same write choke point, both engines.

  • @setup and @verify do what they say in mxcli test — both were parsed into the test case and read by nothing.

    • @setup Mod.Seed names 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 than call 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, @throws and @cleanup describe one test's execution and are refused there by name rather than ignored.
    • @verify runs 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 @verify fail. @cleanup rollback is refused alongside it, s...
Read more