Skip to content

v0.23.0

Choose a tag to compare

@github-actions github-actions released this 21 Sep 11:16
· 217 commits to main since this release

[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 majority and a percentage threshold a Relative one, and that both outcome pointers hold the outcome's $ID. Measured on mxbuild 11.13.0: a fallback is required for consensus, majority and threshold (CE1866) and a veto needs its outcome (CE1867), both refused by check as MDL-WF13 along with a name that is none of the task's outcomes; a decision microflow must return String (CE5012), its parameters are free; and the build does not range-check thresholds or participant counts, so neither does mxcli. Over MCP the backend works around two constructor quirks it measured: PED's default consensus has no fallback, and a more-than-half majority's fallback is dropped and has to be set by $ID afterwards.

  • AI agent tasks in workflows: call agent microflow (docs/11-proposals/PROPOSAL_workflow_studio_pro_constructs.md, phase 2) — the workflow step that runs an AI agent, which describe workflow could only print as a -- [Workflows$AIAgentTaskActivity] comment and a rewrite therefore refused. It is the call-microflow statement with agent added — same as, comment, with (…), outcomes and boundary events — because Mendix stores exactly the call-microflow shape under a different $Type (ako/TestApp, 11.14.0). The rules were measured on mxbuild 11.13.0 by writing each shape as a call microflow and switching only the $Type: every shape builds the same except an agent microflow with no parameters, which fails CE1590, so that is the one new check. Needs Mendix 11.9 (workflows.ai_agent_task); describe emits it, and a rewrite that restates it proceeds.

    Verified in Studio Pro 11.14 over MCP as well as mxbuild: the shapes are accepted, and Studio Pro reports the same on-created and handler signature errors, flags an agent task without parameter mappings, and refuses an invented event type at create.

  • DROP LAYOUT Module.Name (#1063) — layouts were the only document mxcli could create and alter but not delete. Every other doctype has a DROP, and the gap bit hardest on a layout mxcli itself had just written badly: the reported script created a layout that fails the build, and there was no headless way to remove it. The backend could already do it — DeleteLayout is what CREATE OR REPLACE LAYOUT uses for the replace half — so only the verb was missing.

    Pages still bound to the layout are named in a warning and the drop proceeds, matching every other DROP in mxcli (none refuse on dependents) and matching how a layout is normally corrected: re-create it under the same name and the pages rebind by qualified name — verified end to end, back to 0 errors. Left dropped, each page fails CE1613 "The selected layout … no longer exists", which names the page and never the layout, so the warning names the pages and gives the alter pages … where layout = repoint.

  • run --local --db-type hsqldb — boot with no database server — a local run can now use Mendix's built-in, file-based HSQLDB instead of PostgreSQL. The runtime already ships the driver and derives the file URL from DatabaseType and DatabaseName, so the data lands under <project>/deployment/data/database/hsqldb/. --db-host, --db-user and --db-password are refused with HSQLDB (it is a file, not a connection) and --ensure-db is refused too (there is nothing to provision); PostgreSQL remains the default and is unchanged. Intended for local development and demos only — Mendix marks HSQLDB as local-testing-only.

  • ALTER ENTITIES IN <module> ADD ATTRIBUTE … — the bulk form — giving every entity in a module an audit trail cost one statement per entity. Measured on a real module: 10 statements at ~242 tokens against one statement at ~25, and at 200 entities the single-entity form is ~9,700 tokens of near-identical text for an agent to emit and a human to review.

    Scope is deliberately narrow, following ALTER PAGES, which is bulk for exactly one operation: ADD ATTRIBUTE only, because DROP and RENAME aimed at a set are destructive by a typo and SET POSITION on every entity is meaningless. WHERE reuses the persistence words CREATE ENTITY already uses rather than inventing a predicate language, so no new lexer token was needed.

  • ALTER PROJECT SECURITY STRICT MODE ON|OFF — lint rule SEC005 reported "strict mode is disabled" and MDL had no statement that would turn it on; its own suggestion said "not settable via MDL". A rule with no remedy. StrictMode was read everywhere and written nowhere — show security prints it, the Starlark rule lints it, and SetStrictMode existed in gen and was never called.

    The AST field is a pointer, not a bool: "the statement said nothing about it" has to stay distinguishable from "the statement asked for off", or every DEMO USERS toggle would silently disable strict mode.

  • A microflow's URL, export level and concurrency settings are authorable — url 'order/{Order}' / url search parameters ($Tab) / drop url, export level api | hidden, and disallow concurrent execution error message '…' | error microflow Mod.X / allow concurrent execution.

    #1120 made these survive a rewrite. That left the one hole preservation cannot close: describe → rename → exec is mxcli's copy operation, and a copy is a new document with nothing to preserve from — which is why DESCRIBE emitted -- URL: as an apology. It now emits the clauses. An omitted clause still preserves what is stored, so every AST field is a pointer: "not said" must stay distinguishable from "set to the zero value", or the feature reintroduces #1120 through the front door.

  • ALTER PAGE … SET Documentation on a page, layout or snippet — documenting an existing page meant re-running its CREATE, and for a real page that means re-emitting its whole widget tree through a describe → exec round trip that is only as complete as what MDL can spell, so the workaround could silently lose widgets. One case covers all three doctypes. An empty string clears rather than being rejected: removing a doc comment from a script has to be expressible.

  • search by — a List View's search bar — SearchAttributes: on the widget was MDL-WIDGET07, "not recognized and will be silently dropped on write", so the search bar could only be configured in Studio Pro. The clause mirrors sort by — same position on the database source, same comma list, and no direction, because a search attribute is only a name — so one example teaches both. List view only: Forms$ListViewXPathSource is the source type that declares Search, and the grid sources do not.

  • The catalog indexes an entity's audit members and its entity event handlers — CATALOG.ENTITIES gains the four audit members, and CATALOG.ENTITY_EVENT_HANDLERS lists what used to be a bare HasEventHandlers flag. Both close questions that previously returned a wrong answer rather than an error: the LEFT JOIN CATALOG.ATTRIBUTES … a.Name = 'CreatedDate' anyone would write reports every entity as missing an audit trail, forever, because the members are not attributes. Starlark rules get has_created_date, has_changed_date, has_owner and has_changed_by for the same reason — neither attributes_for() nor attribute_count can see them.

  • The System module's enumerations are readable (#1102) — describe enumeration System.WorkflowActivityType answered "enumeration not found" while describe entity System.WorkflowActivityRecord printed ActivityType: Enumeration(System.WorkflowActivityType) in the same session, so a System enum's values could only be guessed at until the build rejected the guess with CE1613. The System module is not stored in the .mpr at all (the string WorkflowActivityType occurs in 0 of 370 units), so the 15 enumerations come from modelsdk/meta.SystemEnumerations — in the tree since #889 with no non-test consumers. Read-only: they are Mendix's, not the project's.

Changed

  • The fluent api/ package takes a backend, not a concrete writer — api.New now takes a backend.FullBackend, with api.Open for the common case. api/ imported mdl/backend zero times: it was not dependent on the legacy backend, it bypassed the abstraction entirely by holding a concrete *mpr.Writer — which is why AddAttribute and UpdateAttribute sat unimplemented on the codec engine with api/ as their only caller. The builder surface is untouched, so builder code does not change; the practical gain is that the same builders now run against any backend, including a live Studio Pro over MCP, which was unreachable before. Open owns the connection it makes; a backend passed to New belongs to the caller and Close leaves it alone.

  • Workflows and view entities are the default modelling choice in the shipped guidance — both are first-class in Mendix and both get reinvented in microflows by default, because the microflow version works: it passes check, it builds, and nothing flags it. No command can prompt for either — lint cannot know a Status attribute is standing in for a state machine — so the instruction has to be in the guidance, or it reaches an agent only when the user already knew to ask.

Removed

  • The legacy sdk/mpr engine is deleted (phases 1–2 of docs/plans/2026-09-14-retire-legacy-engine.md) — the codec engine has been the default since the 2026-06 cutover and nothing routed to legacy any more. Deleted with it: mdl/backend/mpr, mdl/enginecompare and its make target, and the bson compare command. --engine and MXCLI_ENGINE survive as a warning-only no-op, so existing scripts and CI do not break on an unknown flag.

    Nothing that ships in the binary imports the old package, which makes reaching past the backend abstraction a compile error rather than a rule to remember.

Fixed

  • marketplace install refused a package's own bundled files when the project was named by a relative path (-p app.mpr): InstallPackageFiles joined filepath.Dir("app.mpr") == "." with the entry, which drops the dot, then checked the result against the prefix ./ — so manifest.json was rejected as "would write outside the project". The module had already been transplanted into the model by then, so the command exited 1 over a half-finished install (model updated, no themesource/ or widgets/ on disk). The project directory is now made absolute before the containment check; the zip-slip guard is unchanged and its tests still pass. Found by the first field run of --file; the same code runs for an online install. Test added, shown to fail on the unpatched function with the exact message.

  • The runtime start cycle did not create a database that does not exist yet — RuntimeController.Start only ran execute_ddl_commands when Mendix answered result 3 ("the database has to be updated"). A brand-new database answers result 2 ("the database to be used does not exist"), which was unhandled, so the first boot against the built-in HSQLDB database failed with start failed: The database to be used does not exist. Both results now trigger the schema step, and a start that still fails after it says so explicitly.

  • mxcli run --local hung forever on Windows at Starting mxbuild --serve... — two POSIX assumptions in cmd/mxcli/docker combined. ServeServer.alive() (and LocalRuntime.alive()) asked os.Process.Signal(syscall.Signal(0)), which Go implements as EWINDOWS ("not supported by windows") for every signal but Kill — so a live mxbuild read as dead and waitReady() aborted the boot the instant it launched it. And Stop() waited on cmd.Wait() after a killProcessGroup that on Windows only called p.Kill(): mxbuild.exe is a wrapper that launches a Deno web-ext worker (modeler/tools/deno/win-x64/deno.exe) which inherits the stdout/stderr pipe exec.Cmd hands out, so the orphan kept the pipe open and Wait() never saw EOF — no error, no exit, just a hang. Windows now has a real liveness check (OpenProcess(SYNCHRONIZE) + WaitForSingleObject) and a tree kill (taskkill /F /T) behind signalProcessGroup/killProcessGroup; the POSIX implementations are unchanged. The Windows-only tests in procgroup_windows_test.go pin both halves — the tree-kill test fails on the pre-fix code, verified as a control — and a windows-latest CI job runs them (asserting they actually executed) so neither can come back.

  • raise error; on a microflow's main flow passed check and exec, then failed the build (#1030) — with [error] [CE0710] "The main flow cannot join an error flow or end in an error event.", one per microflow. Mendix's error event re-raises the error being handled, so it is legal only where an error is in scope: inside an on error { … } handler. Studio Pro will not draw the connection from the normal flow to an error event; mxcli could, and did. It is now MDL084, at error severity, so exec's pre-flight refuses the script with nothing written (--no-check still applies it, for reproducing the build failure).

    The report's own diagnosis — a trailing End event appended because RaiseErrorStmt never set the builder's "ends with return" flag — is not the cause: isTerminalStmt has treated it as a terminator all along, and the graph mxcli builds for raise error; is exactly the one the report asks for (start event, error event, one sequence flow, no trailing End event, no outgoing flow), pinned by a test. No wiring makes a main-flow error event legal, which is why the fix is a refusal rather than a builder change.

    Nesting does not exempt anything: a raise inside an if, a loop or a split on the main flow is still on the main flow, while one inside a branch of a handler body is still on the error flow. Rules are covered too — a rule goes through the same flow builder. Measured on mxbuild 11.13.0, two copies of a blank app from a 0-error baseline: the four reported shapes → 4× CE0710; the identical statement inside a handler → 0 errors.

  • Every microflow behind a published REST endpoint read as dead (#1126). The catalog recorded each operation's microflow in published_rest_operations_data.Microflow and emitted no refs edge to it, so a microflow whose only caller is an endpoint had zero inbound references: CATALOG.GRAPH_DEAD_ASSETS listed it, SHOW CALLERS OF answered "(no callers found)", SHOW REFERENCES TO and impact reported nothing, and lint rule QUAL004 said "is not called from anywhere. Remove if unused." On the reported model 92 of 93 operations name a microflow and all 92 were listed dead — 15 percent of its dead-microflow list, aimed at the most exposed code in the app.

    A published REST operation is an entry point of the same shape as a scheduled event — the platform invokes it, so nothing in the model calls it — and now emits a publish edge from the operation to the microflow it runs. The source is the operation, not the service, so show references to names the one endpoint rather than the service holding thirty of them, and the edge carries the operation's catalog id, so its path and summary are one join away.

    Two corrections to the report's diagnosis, both measured. GRAPH_DEAD_ASSETS is kind-agnostic — it asks only whether any refs row targets the name — so the edge alone clears it; the comment beside schedule in graphRefKinds claiming otherwise was never true and is fixed. impact and SHOW REFERENCES TO do not filter by kind either. publish is still added to graphRefKinds, for the analysis graph: without it an API handler is an unreachable root in communities, layers, cycles and centrality.

    Found while auditing the three vocabularies: settings was missing from show callers. The project-settings edge shipped in v0.22.0 into refs and into QUAL004 but not into callerRefKinds, so show callers of <AfterStartupMicroflow> was still blind to it — fixed here too. sync is deliberately not a caller kind: it names an entity an offline profile downloads, which is a use of a type, and the test now pins it in the excluded set beside datasource.

  • The expression type checker was silent on a LOOP variable, and on every locally declared variable (#1100). $out = $out + $r/Status inside LOOP $r IN $reqs — an Enumeration concatenated into a String — passed check --references, was written by exec, and failed the build with CE0117 at the Change variable activity, while the same mistake on a parameter was refused as E004.

    The report reads as "the checker is skipped inside a LOOP body". It is not, and the control says so: 'status=' + $T/Status on a parameter, written one line inside the loop, was refused before this change. The body was always walked; the variable scope had two holes, and both had to close before the reported script reported anything. LOOP $r IN $reqs never recorded $r, so $r/Status resolved to no attribute and inferred Unknown — which every rule tolerates by design. And Context.Scope was never set at all, so a DECLARE $out String was Unknown too; E004 needs both operands typed, so typing the iterator alone still reported nothing. That second hole is why $out = $out + $Req/Status was equally silent with no loop in sight — the report's case A hides it by putting a string literal on the left.

    The list sources a loop can iterate are typed with it: a database retrieve, an association retrieve (the far end resolved through the association index, which needs parameters seeded before the body walk), a CREATE LIST, and the list operations that carry their input's element type through. Two other block-scoped positions the report asked about are covered: an ON ERROR handler body, which was not walked at all, so moving a statement into one exempted it from every rule; and a FIND/FILTER predicate, where $currentObject is bound to the element type of the list under test. A bare attribute name in a predicate (FILTER($L, Status = 'Open')) still resolves to nothing — binding bare names would change what a bare identifier means everywhere in an expression.

    Measured: exec-then-type-check over 591 mdl-examples/ scripts gives 11 violations before and 11 after, the same rules on the same lines. The sweep earned its keep — a first cut reported Mendix's string find($Hay, $Needle) as a non-Boolean predicate, because the visitor still builds it as a list operation and the flow builder disambiguates it later (ledger #63); a FIND/FILTER predicate is now checked only when the input list's element entity is known, which applies the same disambiguation.

  • mxcli check and the editor could not parse a .test.mdl file at all (#1103). A test block is a microflow body — that is what the runner turns it into — and both were handing the file to the top-level grammar instead. DECLARE is not a top-level statement, so the parser resynced; RETRIEVE is a non-reserved keyword, so it was swallowed as an identifier; and the leftover FROM … started an OQL query, whose follow set is {GROUP_BY, SELECT, HAVING}. The reported error therefore told the author their RETRIEVE needed a SELECT, on a statement mxcli syntax microflow.retrieve prints as its own example.

    This was not a corner: the VS Code extension binds MDL to .mdl, which .test.mdl matches, so every test file open in the editor was a wall of squiggles — 9 of this repository's 10 test files reported errors, one of them 392, now all 0. Each block is rendered as the microflow it becomes, padded so it keeps its source line numbers, which is what lets every existing rule apply with no remapping: mxcli check suite.test.mdl now reports an uncompilable body, an unusable @expect or @verify (MDL-TEST01), and everything else, at the line the author wrote it on. make check-mdl sweeps test files too, with .fail.test.mdl for one whose annotations are deliberately unusable.

  • retrieve … limit 1 silently binds a single object, and nothing said so until the build (#1103). It is Mendix's "First object" range, so head(), count() or a loop over that variable is CE0097 — but check --references passed, and describe re-emits limit 1, so an object retrieve and a list retrieve are identical MDL text. MDL-RETRIEVE01 reports it at check time, naming the CE code and both working spellings. The behaviour itself is unchanged and still documented; only the silence is fixed. (The same word means the opposite on import from mapping, where first binds the object and limit 1 a one-element list — which is why reading it as a list is a reasonable mistake.)

  • mxcli test reported a rejected build as one sentence, and one leftover document then failed every later run of any test file (#1104).

    MxBuild puts the same text in Message for every failing build, so "build failed: The project cannot be deployed, because it contains errors." cannot distinguish "your test does not compile" from "an unrelated document is broken". The parsed problems were in hand and discarded one line before they were needed: --attach and every --watch rebuild built their error with fmt.Errorf, while the attribution that turns a build error into the failing test's row was wired only into the --local boot. Both paths now carry the problems, so a build error in a generated test microflow becomes that test's ERROR row and one in the project names its document.

    Cleanup now removes every generated MxTest.Test_* microflow the project holds rather than the current suite's. The names are positional (Test_test_1, _2, … from the test's index in its file) and every test file reuses them, so a run with fewer tests than the last one left the surplus behind — and under --attach the MxTest module always pre-exists, so the whole-module drop never fired. A single leftover that does not build then failed every subsequent run of every file, reporting a problem in the project rather than in any test. Keying on the suite was also wrong the other way: it issued DROP for flows a part-way injection never created, and those failures made cleanup announce "the project has been left modified" for a project it had just cleaned. What cleanup genuinely cannot remove is now named, with the DROP that removes it.

  • check --references rejected every XPath constraint that hops an INHERITED or a CROSS-MODULE association (ako/mxcli-sudoku FINDINGS #57). The constraint-member check added in 3aa2ee0e reported the stock Administration.Account_Overview page — Administration.Account extends System.User, so System.UserRoles (declared from System.User) is an association of it by inheritance, and mx check on the rejected page says 0 errors. Because the false positive lands on a Marketplace module almost every app has, check stopped being usable as a gate for anyone whose entities inherit, which is the normal case for anything extending System.User, System.Image or System.FileDocument.

    The lookup matched the start entity against the association's two ends by exact equality and read only dm.Associations. Its comment said the specialisation case was deliberately not chased — "the cost of being wrong is a false error on a working script" — and that was sound while its only caller treated false as silence; the new check treated the same false as evidence, so the precise case the comment declined to chase became the finding. It is three-valued now (resolved / missing / not-an-end / unknown): the start entity is matched through its generalization chain, a cross-module association is found where it is actually stored (CrossAssociations, far end held by name), and a chain that could not be walked to its root is silence rather than a report. The check still fires on an association the entity genuinely lacks, including a specialisation's association named on its generalization.

    Resolving the hop also types the far end, so steps after an inherited association are now checked at all — a retrieve $Roles from $Account/System.UserRoles followed by a bogus member on System.UserRole was silent before and is reported now.

  • Every notify workflow mxcli wrote failed the build, and its output variable was lost on read. Mendix requires a notify to name its target — measured on the 11.6, 11.10 and 11.13 mxbuilds, one without is CE0166 ("The 'Target' property is required"; "'Activity'" on 11.6) — and MDL had no way to name one, while describe dropped the stored target so a rewrite removed it. check and exec now refuse a notify with no target (MDL-WF16). Separately, modelsdk/gen bound the output variable to the key VariableName where Mendix stores OutputVariableName (the 11.6 metamodel and Studio Pro 11.14 agree), so the reader returned it empty; under that key the variable is undefined in the build (CE0109). Fixed as a storage-name override.

  • A workflow rewrite deleted every notification activity without a word. describe workflow printed a Workflows$NotificationActivity as a -- [Workflows$NotificationActivity] comment and the rewrite guard did not count them, so create or modify from that output removed it and reported "Created workflow". Measured with the previous build: one stored, zero after the rewrite. Notification activities are described as statements now, and a rewrite that does not restate them is refused.

  • Over MCP, a user task's notification boundary events would have been lost without a word. Studio Pro 11.14's single user task constructor drops boundaryEvents — at create and when an update adds the task — and reports "No errors found." Measured live; a multi-user task keeps them, and a notification event added afterwards at the task's boundaryEvents is stored, so the backend adds them after each create and update when the stored list is empty. Its interrupting constructors also rewrite a boundary path's ending (a jump to outside a parallel split becomes an End), so a notification path Studio Pro would rewrite is refused with the ending it needs. Timer boundary events are affected by the same constructors and are not yet handled.

  • Multi-user tasks could not be created over MCP on Studio Pro 11.14. Its multi-user task constructor takes the page as a taskPage element and refused the bare pageReference string mxcli sent ("/flow/activities/1/taskPage":"Expected an object, but the value is missing."). The backend reads the constructor schema and sends the shape the server declares, as it already did for the workflow's own constructor.

  • A workflow rewrite reset every multi-user task's participant count and "wait for all users" without a word. The rewrite guard refused a non-default completion rule, but a task needing 3 participants or 80 percent, or one waiting for all users, was rebuilt as "all participants, not waiting" and exec reported success. Both are expressible now, and a rewrite that does not restate them is refused.

  • Workflows written over MCP lost their on-created microflows and event handlers, and could not be created at all on Studio Pro 11.14. The MCP mapper hard-coded onCreatedEvent: NoEvent and sent no onWorkflowEvent, so both constructs from the previous release were dropped without a word; it now sends MicroflowBasedEvent and the handlers, and replaces stored handlers on a rewrite. Separately, Studio Pro 11.14's workflow constructor takes the context entity as context and workflowName as a string and rejects the older payload ("/context":"Expected reference (string), got undefined"), so every workflow create over MCP failed; the backend now reads the constructor schema and sends whichever shape the server declares. AI agent tasks are sent as Workflows$AIAgentTaskActivity.

  • describe workflow turned a call microflow's caption into the microflow's name on re-execute. The caption was printed only as a trailing -- caption comment, which the parser discards, so describe → exec replaced comment 'Summarise the order' with ACT_Summarise. An authored caption is now described as comment '…'; the derived default is not. Found by the AI agent task's end-to-end round trip.

  • A layout whose placeholder is not named Main passed check AND exec, then failed the build (#1063). Exactly one placeholder must be named Main — measured on Mendix 11.12.1 against a layout no page uses, so it does not depend on a page binding: none → CE0848, two → CE0849, and a repeated name of any kind → CE0495. Extra placeholders under other names are fine. Now reported as MDL081 (the Main rule) and MDL082 (duplicate names) by mxcli check with no project needed, and refused by the writer.

    The root cause was a documented belief, not an oversight: mxcli's own docs called the name a convention ("22 of 22 Atlas layouts follow it"), and the write-time guard implemented exactly that — it accepted any placeholder under any name. mxbuild validates it as a rule. The rule now lives once, in types.CheckLayoutPlaceholderNames, and is applied by both the checker and the writer so the two cannot drift; CLAUDE.md and the layouts skill are corrected.

  • placeholder Main { … } in a layout passed check, then failed exec after creating the module (#1063). A layout declares a placeholder with no body; the braced form is the page-side spelling that fills one, and the visitor drops it — so the layout ended up with no placeholder at all and the write failed with "declares no placeholder", contradicting a script that plainly said placeholder Main. Reaching for page syntax here is the natural mistake, since every other layout element takes a body. Now reported as MDL083 at check time, before anything is written.

  • describe page → exec silently lost six things on a Studio Pro-authored page, with mx check at 0 errors on both sides. The one that matters: IsPasswordBox True → False — a password field round-trips into a plaintext text box, so copying a login or change-password page lost it silently. Measured on ako/TestApp's Administration.ChangePasswordForm at 11.14.0; four different causes behind one symptom. Three siblings of the same class:

    • A DataGrid2 column's filter was dropped whenever the column also had custom content. A column has two Widgets-typed slots and the reader took the first one it met instead of keying on the resolved property key; the writer emits column properties alphabetically, so content won. A filter-only column round-tripped by accident, which is why this survived.
    • A check box's ReadOnlyStyle was written as the constant Inherit. It parsed, passed check, and reached a writer that hardcoded the value, so describe → exec downgraded Control to Inherit — and that value is the whole of "show a Boolean as a checkbox" in a DataGrid2 cell.
    • A page rewrite moved state nobody asked to change, so ADR-0008's elision could not fire and the unit churned in version control on every re-run. Fourteen differences in four classes on one page, all "the rebuild writes a constant where Studio Pro stores a value" — CanvasWidth alone takes seven distinct values across 67 pages, and the hardcoded 1200 matched four of them.
  • Image widgets: every dynamic image mxcli wrote failed the build, and a static image could not be described at all. dynamicImageToGen called imageViewerSourceToGen() with no arguments — a zero-parameter constructor for a property whose entire content is the entity binding — so mxbuild answered CE0489 "Select an entity for the data source of this dynamic image." DESCRIBE PAGE could not spell Forms$StaticImageViewer, so replaying the description of a Selection helper with renderStyle: 'custom' emptied its three mandatory slots (CE0642 ×3); the reporter's workaround was an empty dynamictext per slot — structurally valid, icons gone. Both now round-trip, reference and all.

    Because #518 and #538 made both widgets materially easier to author while Mendix may refuse them outright, lint now reports the legacy image widgets and says when CE0582 actually applies. Measured: no validator mentioned either widget, so the only signal was that error at the far end of a build.

  • Page bindings that passed check and exec and then failed the build. Each was silent at every layer mxcli controls:

    • An input widget could not bind an attribute over an association — textbox t (attribute: Assoc/Attr) built a flat path and failed CE1613, the association segment pasted onto the context entity rather than navigated. The same syntax already worked on a DataGrid2 column, so one page could bind an associated attribute in a grid column and fail on the text box beside it.
    • A page parameter passed as an argument to a microflow or nanoflow button action was written as an expression. Mendix stores a flow argument in one of two slots — a reference to a page parameter goes in Variable, a literal or expression in Expression — and the Expression form binds nothing, so Studio Pro reports CE1571 on opening the page.
    • A list widget's own row action was judged in the context outside it. MDL-PAGEARG01 refused onClick: SHOW_PAGE M.Edit(X: $currentObject) with "widget is not inside a data view, list view or grid row" — but a list widget's onClick fires per row, so the row it renders is the context object, and on a listview the refusal contradicted its own wording. Since exec refuses a script whose check reports an error, this was a blocker rather than a warning.
    • A show_page widget argument where there is no context object was dropped in silence — check --references said "All references valid", exec reported success, DESCRIBE PAGE then printed (Item: $currentObject) on a page where $currentObject is unbound, and mxbuild gave one CE1571 per parameter of the target page.
    • A list view template naming the list view's own entity was accepted (CE0543). The guard existed and was one case too generous: it gated on entityIsOrDescendsFrom, which returns true on its first check, where Mendix requires a strict specialization.
    • ALTER PAGE … SET DataSource = DATABASE wiped the source instead of refusing. A DATABASE source has no single stored shape — the widget holding it decides which element Studio Pro writes — and one mapping stood in for several, leaving the DataView with no usable datasource and CE7007 from mxbuild as the only signal, naming the widget and never the statement that broke it.
    • 11.1/10.17 header keys were written into older projects — Page.Autofocus (11.1.0), Page.Variables and Snippet.Variables (10.17.0), all written unconditionally. The follow-up to #1121, which fixed this defect in a page's parameters and left the header out of scope.
    • Action: on a list view or an image widget was reported as unwritable (MDL-WIDGET23) with a wrap-it-in-a-container workaround, when the model already carried the field and the writers already serialised it — one line per widget.
    • Two checks contradicted the writer. MDL-WIDGET20 warned that a List View's Editable is "silently dropped on write and the widget stays enabled"; measured on 11.12.2, exec writes it, describe page reads back Editable: true and mxbuild reports 0 errors. And check --references refused htmlelement, attribute and tagcontentcontainer — MDL that describe page had just emitted — because it read a different widget registry than exec, so exec refused to run the script at all.
    • search by parsed on a gallery and a data grid, where only Forms$ListViewXPathSource declares Search; it is now refused where it cannot be stored (found while reviewing the commit that added it).
    • A design-property SET on a built-in widget dead-ended into property "Remove empty text" not found (widget has no pluggable Object) — words the author never wrote, that cannot be made true by editing the script, and that never named ALTER STYLING as the route that does work.
  • Two security rules made mxbuild refuse the whole module. A third path to CE0066, beside the two in v0.22.0:

    • An access rule on an entity carrying AutoOwner or AutoChangedBy reported the module as CE0066 "Entity access is out of date" — and UPDATE SECURITY, the documented repair for exactly that error, printed "Reconciled 1 access rule(s)" and left it standing, because it re-added the entry that caused it. mxcli wrote a MemberAccess for the implicit System.owner / System.changedBy associations, which Mendix does not store. Four lines on a clean production-security app reproduce it.
    • grant write * kept write rights on an autonumber (CE6592), so the grant had to be narrowed by hand with a REVOKE. The downgrade asked only whether an attribute was calculated; an autonumber carries no CalculatedValue, because its value comes from the database on insert rather than from a microflow on read, so the predicate covered exactly half the rule Mendix applies.
  • DROP ENTITY left cross-module associations pointing at the entity it deleted, and mxbuild 11.14.0 could then not load the project: System.AggregateException … (The given key … was not present in the dictionary.) at StreamingBsonUnitReader.ResolvePostponedProperties() — no CE code, no document named, the failure in the storage layer. The sweep covered only the regular Associations collection, so dropping the local FROM end left a 16-byte pointer to an element that no longer exists.

  • External entities imported from an OData contract were wrong in three ways.

    • A writable service produced one CE6630 per attribute. An external entity's key attributes and its non-key attributes both followed the entity set's UpdateRestrictions; Mendix computes a key as non-updatable whatever the contract says, and a top-level entity has no updatable attributes at all. Fixing only the key took the reported repro from 2 errors to 1 — it now reaches 0.
    • Every property typed as an OData ComplexType was dropped on import (#1118). exec reported "1 created, 0 failed", printed nothing, and mx check said 0 errors, so the loss surfaced later as CE1613 on a page written against the attributes Studio Pro would have made. mdl/types/edmx.go never parsed <ComplexType> at all, so such a property was indistinguishable from one of an unknown type. The first flattening passed clean against a synthetic two-property type; the integration suite's live TripPin contract — which has a base complex type, two derived from it, a nested complex property and an Edm.GeographyPoint — then found four more defects in it, at 11 errors.
    • Edm.TimeOfDay was untyped, and an external action Mendix cannot call is now refused (#1089). CE7252 "The parameters for remote action have changed" survived DROP + CREATE OR REPLACE MICROFLOW and was reported as a stale fingerprint needing a refresh command. No fingerprint is stored — Mendix re-derives the alignment from the cached contract on every build — so recreating the microflow really does rewrite everything the call holds, which means its failure says the rewrite was equally wrong, or the action is not callable at all. Both turned out to be true, on different EDM shapes.
  • Three microflow properties were deleted by any rewrite, including one that edited only the body (#1120). Each had no path across CREATE OR MODIFY MICROFLOW at any of the three layers, and each is silent because what comes out is a valid microflow:

    • the deep-link URL (Mendix 10.6+) — Studio Pro's "URL" field, written back as SetUrl("") unconditionally;
    • the export level, pinned to Hidden by every rewrite, so a module's public surface quietly got smaller — found by the mechanical audit the URL fix prompted;
    • the concurrency settings — the worst of the three by direction rather than severity: a rewrite re-enabled concurrent execution on a microflow that disallowed it, removing a running app's concurrency protection, and dropped the error message with all its translations, the error microflow and MarkAsUsed.
  • A microflow run only by an entity event handler read as dead. The catalog reduced the whole list to a HasEventHandlers flag — some exist, and nothing else — and emitted no refs row, so every consumer of the reference graph agreed the handler microflow was unreachable: show callers answered "(no callers found)", GRAPH_DEAD_ASSETS listed it, and lint rule QUAL004 said "is not called from anywhere. Remove if unused." Handlers are now indexed and emit an event edge.

    Mendix stores the rollback event as RollBack — capital B, confirmed against generated/metamodel, disagreeing with every neighbouring enum in that file. The value is stored verbatim rather than normalised, so a query spelling it the expected way returns zero rows and not an error; that is now documented on the table and pinned by a test.

  • mxcli check accepted two more scripts that could not build.

    • A list operation nested inside another (#1101) — $n = COUNT(FILTER($reqs, …)) passed check, execed with "Created microflow", wrote zero list properties, and failed the build with CE0012 "The 'List' property is required." MDL's expression grammar makes list operations look composable; Mendix's model is not, since each is a separate activity whose list is stored as a variable reference, with no slot for a nested computation. Now refused as MDL-LISTOP02.
    • Design properties on ALTER STYLING (ako#509) — the one statement whose entire job is writing design properties was the one statement MDL-WIDGET11 never looked at, so set 'Remove empty text' = on passed check, wrote, and failed the build with CE6083. A flat multi-select value is refused too.
  • mxcli syntax and the REPL's help took different spellings of the same topic (#1025). mxcli syntax advertises mxcli syntax workflow user-task targeting in its own help; handed over as one string — a quoted copy-paste, a tool wrapper, sh -c — it answered Unknown topic: workflow user-task targeting, spaces and all, because the command joined its arguments on "." and never split them. The REPL had resolved multi-word topics since it was written: one question, two answers. HELP also could not parse the dotted paths the registry itself prints, a hyphen and a dot each being their own token and the grammar rule naming neither; all three spellings now work on both sides.

  • @position was parsed and dropped on eleven workflow statements and on the mapping actions, so rewriting a microflow from its own DESCRIBE output moved every workflow action ~800px right — the first one onto the end event. The visitor's setter and the builder's getter were hand-written type switches with no case for them.

  • Every ALTER WORKFLOW op failed with ambiguous activity whenever the workflow contained a jump to. A jump's caption defaults to its target's name, and both activity resolvers matched name or caption into one pool, so the jump's caption shadowed the target. Name precedence now applies to an unqualified reference only — and @N still counts every name-or-caption match in DESCRIBE order, since scoping it to the chosen tier broke addressing a nested activity as ACT_Process@2, which CI caught.

  • Studio Pro 11.14 broke three more things over MCP, beside the workflow constructors above. CREATE MICROFLOW failed outright — the Microflows$Microflow constructor rejects untyped flows and a {type: …} returnType, and now takes only the canvas — so the shape is detected from the live constructor schema rather than assumed. Timer boundary events were stored wrongly while mxbuild built all of them: neither constructor has firstExecutionTime, so every delay was dropped (CE0126), and the interrupting constructor rewrites the path's terminator. And one ped_update_document batch is not applied in the order sent, which stored a flow's activities reversed, left an old activity in, dropped a new one and made replace activity change nothing — while every update reported SUCCESS.

  • mxcli run --local and mxcli setup on four paths nobody had run.

    • A classic-client app (OptimizedClient = No) exited 1 with "the build did not produce a client" about a deployment whose client was in the same directory. The gate knew two shapes — a rollup config to run, or a bundle mxbuild already wrote — and a classic (Dojo) deployment is a third, with neither, because the classic client has no bundling step in any Mendix version.
    • --mxbuild-path was not registered on run and answered "unknown flag", while the shipped run-local skill, the source comment and two resolution error messages all tell you to pass it. On macOS it was the only advertised way out of a platform mismatch, and the plumbing behind the flag already worked.
    • mxbuild --serve started from the cache rather than the resolved binary, so a Mac with the matching Studio Pro installed could build with a Linux download, or with 11.14.0 against an 11.12.2 project. The half-populated cache directory that causes the miss is mxcli's own.
    • mxcli new --version 10.24.25 died with a bare HTTP 404. Mendix 9 and 10 publish four-part artifact names carrying a build number the release notes never mention — the release called 10.24.25 is mxbuild-10.24.25.122571.tar.gz — while Mendix 11 publishes three parts, and nothing resolved a partial version.
  • DESCRIBE ENUMERATION returned an empty caption for every value on a project whose language is not en_US (#1113). A caption is a Texts$Text and Mendix has no language-neutral text — every translation carries a language code — so a hardcoded en_US lookup missed and every caption rendered as ''. The write side already resolved the project's language (#970) and the widget read side was fixed for the same reason (#702); this was the site neither sweep reached. Applied consistently since: there are no hardcoded en_US reads left in mdl/executor.

  • mxcli report ignored lint-config.yaml and scored against a different rule set than lint. A rule a team had deliberately accepted and disabled still scored against them, and no configuration could move the number — on the reporting project, 61 of 86 findings were two such rules, taking a 99/100 baseline to 66/100. Report also carried its own inline copy of the built-in rule list, one rule behind, so the two commands scored one project against two rule sets. Both are the same root cause: report re-implemented lint's setup instead of sharing it.

  • Page parameters were gated at Mendix 11.0 and are available from 9.4, so no parameterised page could be authored from MDL on any Mendix 10 project — and without one, SHOW PAGE Mod.P ($X = $obj) has nothing to bind to. The 11.0 floor was never measured: it matched an illustrative sample table in a proposal and nothing else. Corrected against the Model SDK's own StructureVersionInfo records, along with microflows.show_page_with_params and pages.page_variables.


New Contributors

Full Changelog: v0.22.0...v0.23.0