v0.23.0
[0.23.0] - 2026-09-21
Headline: The last workflow constructs only Studio Pro could author become MDL, describe → exec stops losing what it just read, and the legacy engine is gone. Workflow event sub-processes, notification events, multi-user completion rules and AI agent tasks are authorable; a wave of page round-trip fixes closes the gap between what mxcli reads and what it writes back — a password field that came back as a plaintext text box is the one to read first; and sdk/mpr is deleted, leaving one engine behind the backend abstraction.
Added
-
marketplace install --file <package.mpk>— installs a.mpkthat is already on disk through the same writer as an online install, with no marketplace lookup and no PAT. The kind is read from the package's ownpackage.xml: a module goes through the transplant writer (storage format preserved, theme modules included), a widget lands inwidgets/, anything else is refused rather than guessed at.--fileand a content id are mutually exclusive, and--versionis refused with--filebecause it selects a marketplace release.This closes the gap the command's own help used to point at:
marketplace downloadcould fetch a package to disk, after which the only routes were Studio Pro ormx module-import— the two paths the help itself explains are worse (module-import rewrites MPR v2 as v1 and refuses theme modules). Every function the online path runs after its download (PackageProject,PerformInstall,InstallPackageFiles,moduleNameFromMpk) already took a plain path;--filehands them one.A module installed from disk carries no marketplace version stamp, because it has no marketplace identity:
PerformInstallskipsStampMarketplaceVersionwhen the version id is empty, soupdateanddiffreport — correctly — that no marketplace content is installed under it, instead of claiming a release while naming none.Field run (Linux, no Studio Pro, mxbuild 11.14.0): a module exported with
mx create-module-packagefrom one Mendix 11.14.0 app, installed with--fileinto a fresh MPR v2 app named by a relative-p— 4 units copied, 6 bundled files installed,mprcontents/intact,SHOW MODULESlists the module with an empty Source column,mx checkreports 0 errors; a second run reports "already installed" and changes nothing. A Mendix 10.6.4 theme package is refused withmx's own message (the reference project is still built bymxat the project's version, somx module-import's window applies) and the app is left untouched. -
Named datasources on pluggable widgets (#1109, follow-up to #643) — a datasource-typed property can be given under its own schema key (
optionsSourceAssociationDataSource: database from Module.Customer) instead of only through the genericdatasource:clause, so a widget exposing several datasources can be given each of them. A mode gated onhasDataSourceis selected by a named datasource too, and the newhasDataSource:<propertyKey>condition says WHICH datasource selects a mode — the legible form for a widget whose modes are exclusive by that (a ComboBox's association vs database), where barehasDataSourcecannot tell them apart and mode order silently decides.A dependent property binds against ITS datasource's entity: the link is the widget package's own (
widget.xml'sdataSource="…"), not mapping order, so a DatagridDropdownFilter'srefCaptionresolves againstrefOptions' entity and itsattragainstlinkedDs'. Verified end to end on a real project: both datasources persisted and the two attributes stored asNamedDS.Order.NumberandNamedDS.Customer.Name.#643's guarantee is unchanged, restated in terms of the value rather than the key: a real datasource under a datasource-typed property is accepted, a scalar there is still MDL-WIDGET05 — it names an entity, cannot be stored as a datasource, and used to pass
checkandexecand fail the build with CE0642. Aliases are covered, so the scalar cannot reach the drop under the other spelling. MDL-WIDGET16 accepts a named option list too; while it read only the generic clause it contradicted MDL-WIDGET05 on the same widget, which blockedexecentirely.On a widget exposing several datasources the generic
datasource:clause is refused, naming the keys to use — previously it filled whichever datasource-typed property Go's map iteration yielded first, so the same script produced different documents from run to run with no error either way.The same per-datasource entity context now applies to object-list items, which run their own resolve loop: a chart series given both a static and a dynamic datasource used to bind its static x/y attributes against the dynamic one's entity, writing a reference to an attribute that entity does not have (measured:
CH.Forecast.Region, CE1613 ×2 from mxbuild 11.6.6). The link was already in hand —ItemPropertyMapping.DataSourcecarries widget.xml'sdataSource="…"and the generated chart definitions already mapstaticDataSourceanddynamicDataSourcewith every dependent's link — it simply was not read. A series configuring one datasource, which is every series in every real chart sincedataSetselects static or dynamic, resolves exactly as before.A linked datasource is refused: a widget.xml
isLinked="true"property is filled from the containing widget (a Data Grid 2 supplies its column filter'slinkedDs), so a.def.jsonmapping one is rejected at build time rather than written. Measured on 11.6.6 — five Studio Pro-authored drop-down filters store it empty, the shipped shape passesmx checkat 0 errors, and a filter written WITH it still fails CE0642 "Property 'Datasource to Filter' is required", because mxbuild resolves the property from the parent rather than reading what is stored.linkedDsis the only linked datasource among the eight multi-datasource widget packages intestdata/expr-checker.describe pageemits the named keys back, so describe → exec keeps each binding on the mapping it came from. A widget with ONE configured datasource keeps the genericDataSource:clause it has always been described with, and an unset second datasource drops out of the count — so the output of every widget shipped today is unchanged. A source whose schema key cannot be resolved falls back to the generic spelling rather than being dropped (#956's silent loss, reached by a new route). The detection runs even when a per-widget extractor already found one, because those know about a single datasource each: the drop-down filter's readsrefOptionsand neverlinkedDs, so a filter carrying both described back as single-source. -
notify workflow … target Module.Workflow.Name(docs/11-proposals/PROPOSAL_workflow_studio_pro_constructs.md, phase 4b) — names the element a notify reaches: a notification-started event sub-process, a notification activity, a notification boundary event or a wait for notification. The statement names the element and mxcli looks up what it is, because Mendix stores a different target type for each (captured from ako/TestApp, whose saved notify microflow is the reader's fixture). An element no notification can reach — a timer-started sub-process, a user task, a timer boundary event — is refused with what it is and the names that would work. Before Mendix 11.7 the action names its wait for notification directly (Activity), and mxcli writes that form there. Describe emits the clause, and the action is now writable over MCP. -
Workflow event sub-processes and notification events (docs/11-proposals/PROPOSAL_workflow_studio_pro_constructs.md, phase 4) —
event subprocess <name> on [non] interrupting notification|timer … { … };after the main body,notification <name>;, andboundary event [non] interrupting notification <name> { … }. Until nowdescribe workflowleft event sub-processes out, printed a notification activity as a comment and dropped notification boundary events, so a workflow with any of them could only be edited in Studio Pro. The shapes come from ako/TestApp, built in Studio Pro 11.14 and saved; the saved document is the reader's fixture, which matters because gen has no type for the timer starts, the notification activity or the notification boundary events. Measured on mxbuild 11.13.0: a sub-process body with no end is CE0105, so mxcli appends the End as it does for the main flow; a jump into or out of a sub-process is CE6682 (MDL-WF05, which now checks each flow as its own scope); a timer start without its expression is CE0126 (MDL-WF14); two interrupting boundary events on one activity are CE6697 (MDL-WF15); a start event named like an activity is CE0495, so names are made unique across the flows. The version gates were measured by loading each construct alone into a blank project: notification-started sub-processes load from 11.10 (11.8 per gen), the notification activity and boundary events from 11.11, timer-started sub-processes only from 11.13. A rewrite must restate them;alter workflow … insert boundary eventrefuses the notification kinds for now. Over MCP they go into the constructor, and a rewrite replaces the stored sub-processes. -
Multi-user task completion rules:
participants,decide by,await all users(docs/11-proposals/PROPOSAL_workflow_studio_pro_constructs.md, phase 3) — how many participants must respond (all, a number, a percentage), how their outcomes decide the task (consensus,majority more than half,majority most chosen,threshold <n> percent|votes, each withfallback '<outcome>';veto '<outcome>';microflow M), and whether it waits for everyone. The shapes were captured from Studio Pro rather than read off gen: the examples were built in Studio Pro 11.14 over MCP, saved to ako/TestApp, and the saved document is now the reader's fixture — which is how "more than half" is known to be an Absolute 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 bycheckas 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$IDafterwards. -
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, whichdescribe workflowcould only print as a-- [Workflows$AIAgentTaskActivity]comment and a rewrite therefore refused. It is the call-microflow statement withagentadded — sameas,comment,with (…),outcomesand 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 —DeleteLayoutis whatCREATE OR REPLACE LAYOUTuses 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 fromDatabaseTypeandDatabaseName, so the data lands under<project>/deployment/data/database/hsqldb/.--db-host,--db-userand--db-passwordare refused with HSQLDB (it is a file, not a connection) and--ensure-dbis 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 ATTRIBUTEonly, becauseDROPandRENAMEaimed at a set are destructive by a typo andSET POSITIONon every entity is meaningless.WHEREreuses the persistence wordsCREATE ENTITYalready 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.StrictModewas read everywhere and written nowhere —show securityprints it, the Starlark rule lints it, andSetStrictModeexisted 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 USERStoggle 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, anddisallow 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 Documentationon a page, layout or snippet — documenting an existing page meant re-running itsCREATE, 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 mirrorssort 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$ListViewXPathSourceis the source type that declaresSearch, and the grid sources do not. -
The catalog indexes an entity's audit members and its entity event handlers —
CATALOG.ENTITIESgains the four audit members, andCATALOG.ENTITY_EVENT_HANDLERSlists what used to be a bareHasEventHandlersflag. Both close questions that previously returned a wrong answer rather than an error: theLEFT 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 gethas_created_date,has_changed_date,has_ownerandhas_changed_byfor the same reason — neitherattributes_for()norattribute_countcan see them. -
The System module's enumerations are readable (#1102) —
describe enumeration System.WorkflowActivityTypeanswered "enumeration not found" whiledescribe entity System.WorkflowActivityRecordprintedActivityType: 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.mprat all (the stringWorkflowActivityTypeoccurs in 0 of 370 units), so the 15 enumerations come frommodelsdk/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.Newnow takes abackend.FullBackend, withapi.Openfor the common case.api/importedmdl/backendzero times: it was not dependent on the legacy backend, it bypassed the abstraction entirely by holding a concrete*mpr.Writer— which is whyAddAttributeandUpdateAttributesat unimplemented on the codec engine withapi/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.Openowns the connection it makes; a backend passed toNewbelongs to the caller andCloseleaves 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 —lintcannot know aStatusattribute 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/mprengine is deleted (phases 1–2 ofdocs/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/enginecompareand its make target, and thebson comparecommand.--engineandMXCLI_ENGINEsurvive 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 installrefused a package's own bundled files when the project was named by a relative path (-p app.mpr):InstallPackageFilesjoinedfilepath.Dir("app.mpr") == "."with the entry, which drops the dot, then checked the result against the prefix./— somanifest.jsonwas 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, nothemesource/orwidgets/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.Startonly ranexecute_ddl_commandswhen 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 withstart 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 --localhung forever on Windows atStarting mxbuild --serve...— two POSIX assumptions incmd/mxcli/dockercombined.ServeServer.alive()(andLocalRuntime.alive()) askedos.Process.Signal(syscall.Signal(0)), which Go implements asEWINDOWS("not supported by windows") for every signal butKill— so a live mxbuild read as dead andwaitReady()aborted the boot the instant it launched it. AndStop()waited oncmd.Wait()after akillProcessGroupthat on Windows only calledp.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 pipeexec.Cmdhands out, so the orphan kept the pipe open andWait()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) behindsignalProcessGroup/killProcessGroup; the POSIX implementations are unchanged. The Windows-only tests inprocgroup_windows_test.gopin both halves — the tree-kill test fails on the pre-fix code, verified as a control — and awindows-latestCI 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 anon 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, soexec's pre-flight refuses the script with nothing written (--no-checkstill applies it, for reproducing the build failure).The report's own diagnosis — a trailing End event appended because
RaiseErrorStmtnever set the builder's "ends with return" flag — is not the cause:isTerminalStmthas treated it as a terminator all along, and the graph mxcli builds forraise 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, aloopor 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.Microflowand emitted norefsedge to it, so a microflow whose only caller is an endpoint had zero inbound references:CATALOG.GRAPH_DEAD_ASSETSlisted it,SHOW CALLERS OFanswered "(no callers found)",SHOW REFERENCES TOandimpactreported 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
publishedge from the operation to the microflow it runs. The source is the operation, not the service, soshow references tonames 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_ASSETSis kind-agnostic — it asks only whether anyrefsrow targets the name — so the edge alone clears it; the comment besidescheduleingraphRefKindsclaiming otherwise was never true and is fixed.impactandSHOW REFERENCES TOdo not filter by kind either.publishis still added tographRefKinds, 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:
settingswas missing fromshow callers. The project-settings edge shipped in v0.22.0 intorefsand into QUAL004 but not intocallerRefKinds, soshow callers of <AfterStartupMicroflow>was still blind to it — fixed here too.syncis 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 besidedatasource. -
The expression type checker was silent on a LOOP variable, and on every locally declared variable (#1100).
$out = $out + $r/StatusinsideLOOP $r IN $reqs— an Enumeration concatenated into a String — passedcheck --references, was written byexec, 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
LOOPbody". It is not, and the control says so:'status=' + $T/Statuson 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 $reqsnever recorded$r, so$r/Statusresolved to no attribute and inferred Unknown — which every rule tolerates by design. AndContext.Scopewas never set at all, so aDECLARE $out Stringwas Unknown too; E004 needs both operands typed, so typing the iterator alone still reported nothing. That second hole is why$out = $out + $Req/Statuswas 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$currentObjectis 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 stringfind($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 checkand the editor could not parse a.test.mdlfile 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.DECLAREis not a top-level statement, so the parser resynced;RETRIEVEis a non-reserved keyword, so it was swallowed as an identifier; and the leftoverFROM …started an OQL query, whose follow set is{GROUP_BY, SELECT, HAVING}. The reported error therefore told the author theirRETRIEVEneeded aSELECT, on a statementmxcli syntax microflow.retrieveprints as its own example.This was not a corner: the VS Code extension binds MDL to
.mdl, which.test.mdlmatches, 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.mdlnow reports an uncompilable body, an unusable@expector@verify(MDL-TEST01), and everything else, at the line the author wrote it on.make check-mdlsweeps test files too, with.fail.test.mdlfor one whose annotations are deliberately unusable. -
retrieve … limit 1silently binds a single object, and nothing said so until the build (#1103). It is Mendix's "First object" range, sohead(),count()or aloopover that variable is CE0097 — butcheck --referencespassed, anddescribere-emitslimit 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 onimport from mapping, wherefirstbinds the object andlimit 1a one-element list — which is why reading it as a list is a reasonable mistake.) -
mxcli testreported 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
Messagefor 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:--attachand every--watchrebuild built their error withfmt.Errorf, while the attribution that turns a build error into the failing test's row was wired only into the--localboot. 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--attachtheMxTestmodule 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 issuedDROPfor 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 theDROPthat removes it. -
check --referencesrejected every XPath constraint that hops an INHERITED or a CROSS-MODULE association (ako/mxcli-sudoku FINDINGS #57). The constraint-member check added in3aa2ee0ereported the stockAdministration.Account_Overviewpage —Administration.Account extends System.User, soSystem.UserRoles(declared fromSystem.User) is an association of it by inheritance, andmx checkon the rejected page says 0 errors. Because the false positive lands on a Marketplace module almost every app has,checkstopped being usable as a gate for anyone whose entities inherit, which is the normal case for anything extendingSystem.User,System.ImageorSystem.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 treatedfalseas silence; the new check treated the samefalseas 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.UserRolesfollowed by a bogus member onSystem.UserRolewas silent before and is reported now. -
Every
notify workflowmxcli 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.checkandexecnow refuse a notify with no target (MDL-WF16). Separately,modelsdk/genbound the output variable to the keyVariableNamewhere Mendix storesOutputVariableName(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 workflowprinted aWorkflows$NotificationActivityas a-- [Workflows$NotificationActivity]comment and the rewrite guard did not count them, socreate or modifyfrom 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'sboundaryEventsis 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 (ajump tooutside 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
taskPageelement and refused the barepageReferencestring 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: NoEventand sent noonWorkflowEvent, so both constructs from the previous release were dropped without a word; it now sendsMicroflowBasedEventand the handlers, and replaces stored handlers on a rewrite. Separately, Studio Pro 11.14's workflow constructor takes the context entity ascontextandworkflowNameas 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 asWorkflows$AIAgentTaskActivity. -
describe workflowturned a call microflow's caption into the microflow's name on re-execute. The caption was printed only as a trailing-- captioncomment, which the parser discards, so describe → exec replacedcomment 'Summarise the order'withACT_Summarise. An authored caption is now described ascomment '…'; the derived default is not. Found by the AI agent task's end-to-end round trip. -
A layout whose placeholder is not named
MainpassedcheckANDexec, then failed the build (#1063). Exactly one placeholder must be namedMain— 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) bymxcli checkwith 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 passedcheck, then failedexecafter 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 saidplaceholder 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→execsilently lost six things on a Studio Pro-authored page, withmx checkat 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'sAdministration.ChangePasswordFormat 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
contentwon. A filter-only column round-tripped by accident, which is why this survived. - A check box's
ReadOnlyStylewas written as the constantInherit. It parsed, passed check, and reached a writer that hardcoded the value, so describe → exec downgradedControltoInherit— 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" —
CanvasWidthalone takes seven distinct values across 67 pages, and the hardcoded 1200 matched four of them.
- 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
-
Image widgets: every dynamic image mxcli wrote failed the build, and a static image could not be described at all.
dynamicImageToGencalledimageViewerSourceToGen()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 PAGEcould not spellForms$StaticImageViewer, so replaying the description of a Selection helper withrenderStyle: 'custom'emptied its three mandatory slots (CE0642 ×3); the reporter's workaround was an emptydynamictextper 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,
lintnow 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
checkandexecand 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 inExpression— and theExpressionform 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'sonClickfires per row, so the row it renders is the context object, and on alistviewthe refusal contradicted its own wording. Sinceexecrefuses a script whose check reports an error, this was a blocker rather than a warning. - A
show_pagewidget argument where there is no context object was dropped in silence —check --referencessaid "All references valid",execreported success,DESCRIBE PAGEthen printed(Item: $currentObject)on a page where$currentObjectis 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 = DATABASEwiped 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.VariablesandSnippet.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
Editableis "silently dropped on write and the widget stays enabled"; measured on 11.12.2, exec writes it,describe pagereads backEditable: trueand mxbuild reports 0 errors. Andcheck --referencesrefusedhtmlelement,attributeandtagcontentcontainer— MDL thatdescribe pagehad just emitted — because it read a different widget registry than exec, so exec refused to run the script at all. search byparsed on a gallery and a data grid, where onlyForms$ListViewXPathSourcedeclaresSearch; it is now refused where it cannot be stored (found while reviewing the commit that added it).- A design-property
SETon a built-in widget dead-ended intoproperty "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 namedALTER STYLINGas the route that does work.
- An input widget could not bind an attribute over an association —
-
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
AutoOwnerorAutoChangedByreported the module as CE0066 "Entity access is out of date" — andUPDATE 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 implicitSystem.owner/System.changedByassociations, 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 noCalculatedValue, 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.
- An access rule on an entity carrying
-
DROP ENTITYleft 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.)atStreamingBsonUnitReader.ResolvePostponedProperties()— no CE code, no document named, the failure in the storage layer. The sweep covered only the regularAssociationscollection, 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).
execreported "1 created, 0 failed", printed nothing, andmx checksaid 0 errors, so the loss surfaced later as CE1613 on a page written against the attributes Studio Pro would have made.mdl/types/edmx.gonever 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 anEdm.GeographyPoint— then found four more defects in it, at 11 errors. Edm.TimeOfDaywas untyped, and an external action Mendix cannot call is now refused (#1089). CE7252 "The parameters for remote action have changed" survivedDROP+CREATE OR REPLACE MICROFLOWand 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.
- 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
-
Three microflow properties were deleted by any rewrite, including one that edited only the body (#1120). Each had no path across
CREATE OR MODIFY MICROFLOWat 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
Hiddenby 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.
- the deep-link URL (Mendix 10.6+) — Studio Pro's "URL" field, written back as
-
A microflow run only by an entity event handler read as dead. The catalog reduced the whole list to a
HasEventHandlersflag — some exist, and nothing else — and emitted norefsrow, so every consumer of the reference graph agreed the handler microflow was unreachable:show callersanswered "(no callers found)",GRAPH_DEAD_ASSETSlisted it, and lint rule QUAL004 said "is not called from anywhere. Remove if unused." Handlers are now indexed and emit aneventedge.Mendix stores the rollback event as
RollBack— capital B, confirmed againstgenerated/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 checkaccepted 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, soset 'Remove empty text' = onpassed check, wrote, and failed the build with CE6083. A flat multi-select value is refused too.
- A list operation nested inside another (#1101) —
-
mxcli syntaxand the REPL'shelptook different spellings of the same topic (#1025).mxcli syntaxadvertisesmxcli syntax workflow user-task targetingin its own help; handed over as one string — a quoted copy-paste, a tool wrapper,sh -c— it answeredUnknown 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.HELPalso 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. -
@positionwas 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 WORKFLOWop failed withambiguous activitywhenever the workflow contained ajump 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@Nstill counts every name-or-caption match in DESCRIBE order, since scoping it to the chosen tier broke addressing a nested activity asACT_Process@2, which CI caught. -
Studio Pro 11.14 broke three more things over MCP, beside the workflow constructors above.
CREATE MICROFLOWfailed outright — theMicroflows$Microflowconstructor 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 hasfirstExecutionTime, so every delay was dropped (CE0126), and the interrupting constructor rewrites the path's terminator. And oneped_update_documentbatch is not applied in the order sent, which stored a flow's activities reversed, left an old activity in, dropped a new one and madereplace activitychange nothing — while every update reported SUCCESS. -
mxcli run --localandmxcli setupon 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-pathwas not registered onrunand 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 --servestarted 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.25died 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 ismxbuild-10.24.25.122571.tar.gz— while Mendix 11 publishes three parts, and nothing resolved a partial version.
- A classic-client app (
-
DESCRIBE ENUMERATIONreturned an empty caption for every value on a project whose language is noten_US(#1113). A caption is aTexts$Textand Mendix has no language-neutral text — every translation carries a language code — so a hardcodeden_USlookup 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 hardcodeden_USreads left inmdl/executor. -
mxcli reportignoredlint-config.yamland scored against a different rule set thanlint. 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 ownStructureVersionInforecords, along withmicroflows.show_page_with_paramsandpages.page_variables.
New Contributors
- @MendixMau made their first contribution in #1151
Full Changelog: v0.22.0...v0.23.0