Repository navigation
Sync ako/mxcli: loop-body splices and #1517 decisions, workflow texts and timers, Java action round trips, input widget drops - #1660
Merged
Merged
Conversation
…xt (#1695) An `@annotation` in an `alter microflow|nanoflow … insert|replace` fragment could not refer to a note already in the flow. `@annotation(id: n1)` was refused at exec ("refers to an annotation that has not been declared"), because a fragment is built alone, and check passed it as "not predicted". The `id:` labels describe prints are re-derived on every describe and stored nowhere, so they cannot address a stored note anyway. From mendixlabs#1517, comment item 3. What: a new key, `@annotation(existing: 'Validates the order')`, names a stored note by its text. The text must match exactly one note of the flow (CRLF/LF line breaks match); none or several is refused by check and exec alike, through one function (resolveStoredNotes, called by plan and before check's pre-build loop), the message listing the candidates with where each is drawn and what it is attached to. The note keeps its $ID, text, position and size: the builder links its ID, the line is taken out of the fragment and drawn after the splice by a new MicroflowMutator.LinkNote, which skips a line that is already there (a replace carries the target's line) and orients it on the post-splice geometry with microflows.FacingSides — the executor's formula, moved to sdk/microflows so builder and splice share one copy. `existing:` is its own key, never `text:`, so attaching cannot be read as creating; it never creates a note, stands alone (beside id:/text:/position:/ size: it is MDL079 at check and refused by the builder at exec), and only an alter fragment takes it (a whole-flow statement declares its notes: MDL079). describe never prints it. The visitor never classes it as a free note. ValidateAlterFlow now also checks note labels per fragment, so an undeclared `id:` is refused by check too, with a hint at `existing:`. Verification: executor tests on PedApp copies (attach keeps the note's $ID, position and size and adds the line, with a control where `@annotation 'text'` makes a second note; line orientation; replace dedupe; no-match and ambiguous refusals by check and exec with nothing written; misuse; fragment `id:` regression), LinkNote and visitor unit tests. Revert proofs: every new test fails against the baseline production code or a targeted stub. Real app (11.13.0): the issue's scenario before (MDL079, `--no-check` wrote no note) and after (one shared note, stored note BSON byte-identical), describe → exec Unchanged, mx check 0 errors; TestMxCheck_DoctypeScripts/02-microflow-examples 0 errors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lace body of (#1693) `create or modify microflow` (mdl 1) and `alter microflow` refused any change of a loop body's shape — an activity inserted, dropped or moved — with "the Loop … changes inside its body; the splice does not edit inside a loop", and the advice was `alter … replace loop`, which re-mints every element ID in the loop. Maintainer decision on the issue: splice plain activities inside a body (option c), add an explicit `replace body` as the fallback (option a); do not replace the whole loop implicitly (option b). What changes - mfmutator: room-making, room checks and pins are scoped to a collection — the top level or one loop's body, in the loop box's coordinates. Insert after/before, replace and drop work on body activities, the body's first and last included (appendInLoop, prependInLoop, dropInLoop, replaceInLoop by several). An edited body is fitted back into its box (fitLoopTo): the box grows on its far side along the line the loop sits on, what is drawn past it moving along (and an enclosing loop growing in turn), and symmetrically across that line so the flows in and out stay level. PinInLoop pins a body's stated positions under mdl 1, as Pin does at top level (#1598), so a restated describe never comes back "moved" on its next run. A fragment that is not plain activities is refused in a body (inBodyFragment). - ReplaceLoopBody (new MicroflowMutator method): keeps the loop — $ID, position, iterator, list, flows and notes around it — and replaces only its contents with the body of the loop the builder made of the stored header restated with the new body; the box keeps its size when that fits and grows when it does not. Refused over MCP, like every removal. - create or modify: a loop restated with a changed body is paired with the stored one (lcsStatements) and its body diffed in place (inLoopBody, bodyGap): LCS over the body, plain activities only. A decision, loop, return, break/continue, raise error or error handler added, changed or taken out there, or a body node moved, is refused naming the innermost loop: "the Loop at (…) changes inside its body: a decision is added to it; inside a loop body the splice inserts, drops and replaces plain activities only … `alter microflow M.F { replace body of loop $It in $Items with begin … end; };`". An empty stored body is filled by replace body, which loses nothing there. - alter: `replace body of <loop> with begin … end;` (grammar, AST AlterFlowReplaceBody, visitor, executor replaceBody); the body is scope- checked like a fragment and may not redeclare a variable the flow has outside the loop. In-body operations are checked first (inBodyTarget, notPlainFragment) so a refusal names the real reason and replace body. check -p builds a replace-body fragment the same way exec does. Verification - Tests first on PedApp copies (flow_splice_loop_body_1693_pedapp_test.go): insert middle/first/last/two, drop middle/first/last, swap, replace by two, box growth, nested loop, every alter form, stated positions pinned, replace body (loop $ID kept, iterator kept, box kept when it fits, while loop, nested loop, empty body filled, describe round trip, three refusals), and the refusals that remain — each asserted for check and exec, nothing written. Only the changed activities' $IDs change; control: `replace loop` of the same change re-mints the whole body. Re-running each script is Unchanged with identical bytes. mfmutator unit tests for the geometry (splice_loop_test.go). - Revert proofs: with the production files reverted every new executor test fails with the reported refusal ("the Loop at (780, 200) changes inside its body"; "alter does not splice inside a loop body"); stubbing fitLoopTo, dropInLoop and body pins fails the matching mfmutator tests. - Real app (blank 11.13.0): baseline refuses the repro scripts; new binary splices them — insert: 43 element IDs kept, 0 gone, 6 added (the activity and its flow); alter insert after last, drop first; replace body keeps the loop's $ID (30 kept, the 20 body elements replaced); only the microflow's unit (and Base.mpr) written; every re-run Unchanged with a clean git status; describe → exec Unchanged; mx check 0 errors. - TestMxCheck_DoctypeScripts 02-microflow, 02b-nanoflow, 41-error-handling and the new 02d-microflow-splice-examples: pass. describe → exec over all 29 PedApp flows: Unchanged. mdl/roundtrip integration suite: pass (tests that used a plain insert in a loop as the refused change now add a decision). Docs: alter-microflow reference page (docs-site), quick reference, syntax help, write-microflows skill and pitfalls §11, bug-test and doctype scripts, finding. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The new `existing:` paragraph took the skill's body from 700 to 707 lines, which TestLargeSkillsWereSplit refuses (the body is loaded whole every time the skill is used). The detail already lives in reference/pitfalls.md §14, so SKILL.md keeps a one-line pointer on the paragraph about notes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After another script spliced a decision into a microflow (`alter microflow
… insert after $X begin if … end if; end`), re-running the original
`create or modify` script was refused under mdl 1 — "cannot drop the
ExclusiveSplit … only an activity or a loop can be, because a decision or an
end event changes the shape of the flow" — and so were `alter … drop` and
`alter … replace` of the decision. Maintainer decision on the issue: support
exactly the shape `alter … insert` produces; refuse every other shape with a
specific reason.
What changes
- mfmutator (splice_decision.go): Drop and Replace of a split check that it
has that shape — two paths of plain activities (or none), each node entered
only along its path, no error handler, rejoining at one merge that only
those two paths enter and that leads on to one node (insertedDecision).
Drop joins the flows into the split to the merge's successor (keeping their
$IDs, taking the end the merge's flow had); Replace puts the fragment where
the split began, moving what is past the merge along only if it is longer.
The split, the path activities, the merge and the flows between them go;
notes attached only to path nodes go with them. Any other decision is
refused naming the part out of reach: "its true path ends in a return",
"… holds a decision", "… holds a loop", "… has an error handler",
"N flows join at its merge", "it has N paths", "it splits by type".
- MicroflowMutator.DecisionNodes: the path activities a drop or replace of a
decision takes out. alter judges their outputs like the target's
(checkOutputUnused / noteRemoved), so a variable a path declares that the
flow still reads is refused "still used by".
- The statement diff already emitted Drop/Replace for the stored `if`, so
create or modify needed no change; check -p and exec share the verdict
because both run the mutator.
Verification
- Tests first (flow_splice_decision_1694_pedapp_test.go, PedApp): re-running
the original script after four inserted shapes (if/else, then only, two
activities on a path, else only) reports "spliced: 1 dropped", leaves every
original $ID and nothing else, and a second re-run is Unchanged with
identical bytes (control: a replaced activity shows as 1 gone, 1 added);
alter drop, alter replace, and create or modify replacing the decision;
five out-of-shape decisions refused by check and exec alike for the re-run,
drop and replace, writing nothing; a path output still read refused.
mfmutator unit tests for the shape, geometry and notes.
- Revert proofs: production files reverted — every new executor test fails
("cannot drop the ExclusiveSplit … only an activity or a loop can be");
the decision routing stubbed out of Drop/Replace — every mfmutator decision
test fails.
- Real app (blank 11.13.0): baseline refuses the re-run, the drop and the
replace; new binary: re-run drops the decision — the flow is back to its 29
original element IDs, the 24 inserted ones gone — and a second re-run is
Unchanged with a clean git status; alter drop: 29 kept, 0 added; alter
replace: 29 kept, 6 added; describe → exec Unchanged; only the microflow's
unit (and Base.mpr) written; mx check 0 errors. Bug-test and doctype
scripts run twice on a fresh app: mx check 0 errors.
- TestMxCheck_DoctypeScripts (02, 02b, 41, 02d) and the mdl/roundtrip
integration suite pass; describe → exec over the 29 PedApp flows Unchanged.
Docs: alter-microflow reference page, quick reference, syntax help, pitfalls
§14 (SKILL.md net zero lines), bug-test and doctype scripts, finding.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…de its block check-skill-mdl-semantics.sh skips a deliberately wrong example only when the marker is inside the code block; the ❌ sat in the sentence above it, so CI ran the refused fragment as a real script and failed on MDL079 — the refusal this PR adds to check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…1653) A tab page's Badge (Forms$TabPage.Badge, a client template) had no MDL spelling: check warned MDL-WIDGET07 that `Badge:` would be dropped, describe printed nothing for it, and re-executing describe's output stored "Badge": null, so a badge set in Studio Pro was removed by any rewrite of its page (mendixlabs#1644). `tabpage tp (Caption: 'Open', Badge: '{1}', BadgeParams: ({1} = Count))` now writes it, in the shape of a dynamic text's Content/ContentParams, with the parameters resolved against the enclosing data container, and describe prints it. The keys are exempt from MDL-WIDGET07 on a tab page only; BadgeParams without a Badge, or a Badge that is not a quoted template, is refused (MDL-WIDGET51). On ako/TestApp the seven Studio Pro badges of WorkflowCommons.TaskInbox and TaskDashboard now survive describe -> exec; before, all were nulled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUbjJ5DeS2vysn9hfKir4Y
The docs-site synopsis is grammar with <target> placeholders, not runnable MDL, so it is fenced as text, as the gate advises; the `microflow.alter` syntax example gains the `mdl 1;` header every complete example carries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o buttons (#977) A Studio Pro radio button group stored with ReadOnlyStyle Text came back from describe → exec as Inherit, and the same on the other three input widgets: the writer kept the hard-coded Inherit that #490 removed for the check box, the model had no field, and describe read the key only on the check box and the data view. The four types now take ReadOnlyStyle: Inherit | Control | Text, written, described when not Inherit (the default on 236/237 text boxes, 57/58 text areas, 51/59 date pickers and 4/4 radio button groups in PedApp and TestApp), and checked against one vocabulary (pages.ReadOnlyStyles). `alter page … set (ReadOnlyStyle: …)` was refused on every built-in widget, check box and data view included; it now writes the stored property. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DRueZhZoBML5iWuJTvUnjL
The page #977 named as blocked by the ReadOnlyStyle drop now passes getput: its three date pickers and its text box store ReadOnlyStyle Text. The allowlist may only shrink. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DRueZhZoBML5iWuJTvUnjL
feat(pages): spell a tab page's Badge in MDL, so a rewrite keeps it (#1653)
fix: carry ReadOnlyStyle on text box, text area, date picker and radio buttons (#977 item 1)
The rebuild writes the one language MDL carries, and CarryTranslations appended the stored others after it in sorted order. A text Studio Pro stored as [nl_NL, en_US] came back [en_US, nl_NL], and en_US,fr_FR,nl_NL, de_DE came back sorted. The canonical form compares lists in order, so the reorder alone made an unchanged page a write. The carry now follows the stored text's language order, from whichever pairing found it (path, owning element or source string). Studio Pro's order is not sorted: 129 texts in PedApp and TestApp are stored Dutch first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DRueZhZoBML5iWuJTvUnjL
…1704) describe prints a multi-placeholder page's `placeholder X { … }` blocks in stored order, and the builder put Main first whatever the blocks said, so JTSBootLogboek.Home_Native, stored [Header, Main], came back [Main, Header] and describe → exec rewrote it. Main now comes first only when no block names it, which keeps every page without an explicit Main block as before. The round-trip test covers both causes of #1704 on a Studio Pro-shaped page (arguments [Header, Main], texts stored Dutch first): with either fix reverted it reports "Replaced page". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DRueZhZoBML5iWuJTvUnjL
FeedbackModule.PopupFailure, PopupSuccess, ShareFeedback, ShareFeedback_Logo, MyFirstModule.Home_Web and Administration.ReadMe now pass getput on both PedApp and TestApp: their texts are stored with the languages in an order the translation carry used to sort (#1704). The PedApp list's snippet section is empty now and goes with them. The allowlist may only shrink. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DRueZhZoBML5iWuJTvUnjL
feat: attach an alter fragment's statement to a stored note by its text (#1695)
…body-and-decisions # Conflicts: # .claude/skills/mendix/write-microflows/reference/pitfalls.md # cmd/mxcli/syntax/features_microflow.go # docs/01-project/MDL_QUICK_REFERENCE.md
FeedbackModule.ShareFeedback's whole-document check now passes: the losses its entry listed were fixed earlier, and the last difference was the language order of its texts, which the translation carry now keeps (#1704). With that fix reverted the check fails again. The test fails in both directions, so the entry goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DRueZhZoBML5iWuJTvUnjL
fix: keep the stored placeholder and translation order on describe → exec (#1704)
…m 4) A Java date pattern quotes literal text: `h 'at' mm`. describe printed it unescaped, `customDateFormat: 'h 'at' mm'`, which ends the string at `h `. The write side was wrong too: the format block's string had its outer quotes trimmed and was never unescaped, so 'h ''at'' mm' was stored as `h ''at'' mm`, a literal apostrophe in the pattern. The visitor now reads the value like any other string literal (unquoteStringLit), and describe writes it with mdlQuote, as the date picker's CustomDateFormat already did. Each half alone still fails the round-trip test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DRueZhZoBML5iWuJTvUnjL
item 5) `alter page … set (CustomDateFormat: '') on dp` on a picker already Custom was written, and mxbuild then reports CE0493 "Date format is custom but no format string is specified" (measured on 11.13.0). The only guard sat in the DateFormat setter and relied on name-order application, so clearing the pattern alone never reached it. The rule relates two properties, so it is now checked on the widget the whole SET leaves behind, for statements that name DateFormat or CustomDateFormat. Clearing the pattern and switching to Date in one statement stays valid. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DRueZhZoBML5iWuJTvUnjL
…em 2) describe printed neither, nothing parsed either, and the writer wrote 5 lines and no Autocomplete key whatever was stored. In PedApp and TestApp a text area stores 5 lines on 52 of 58, 2 on 4 and 0 on 2, and Autocomplete true on all 58; a Studio Pro text area of 2 or 0 lines, or with Autocomplete off, came back from describe → exec as 5 and on. mx check is 0 errors either way. `textarea (NumberOfLines: n, Autocomplete: true|false)` is now written, described when not the default (5, true), and settable with `alter page … set`. The model's `Rows int`, which nothing set and which could not hold a stored 0, becomes `NumberOfLines *int`. Autocomplete is written on every text area, as Studio Pro does (introduced in 8.10.0, like the text box's). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DRueZhZoBML5iWuJTvUnjL
`targeting users xpath [System.User/Name = 'MxAdmin']` passed `check --references` and exec, and Studio Pro reported CE0161 on every task carrying it. Nothing walked a targeting constraint, and the shared XPath member walk read an entity as the first path step as a cast. The reference pass now checks each targeting constraint, as buildUserTask stores it, against the workflow user entity (System.User or the App Settings specialization) or System.WorkflowGroup, with the System module's members from modelsdk/meta. It reports a leading entity step, a member the entity lacks, and a missing association. A retrieve and a widget's database source now report a leading entity step too. The access-rule check keeps the plain walk (unmeasured there). Measured with mx check on mxbuild 11.15.0: the three rejected shapes are CE0161, and the role form [System.UserRoles = '[%UserRole_X%]'] is clean and selects the role's users at runtime (11.13.0). A CE1613 in the app hides every CE0161 from mx check, which is recorded in the finding. Fixes #1711 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019xfGeNtrQ5BJrH2Ew64rt5
…io Pro does An interrupting boundary path that runs to its last activity was ended with the end-of-path marker, the terminator Studio Pro writes for a non-interrupting path. Studio Pro ends an interrupting path with an End (a jump inside a parallel split), so a workflow opened there showed the two mixed up, and MCP refused the same MDL. Measured first on the Mendix 11.13.0 runtime: marker and End behave alike on an interrupting path (instance Completed, nothing after the task runs), so this changes the stored shape, not what runs. A post-build pass now gives each interrupting path that runs to its end an End wherever an End may stand: the main flow, outcomes, interrupting paths and event sub-process bodies. Under a parallel split or a non-interrupting path an End is CE1844, so those keep the marker. ALTER uses the target's stored placement for inserted activities and for an inserted boundary event. describe now prints `end workflow;` there, and the describe -> exec round trip reports Unchanged. mx check: 0 errors on 11.15.0 for every placement. Fixes #1712 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019xfGeNtrQ5BJrH2Ew64rt5
…ton per outcome Agents following write-workflows generated task pages with no outcome buttons, so no user could finish a task, and added a microflow per outcome. The skill never mentioned the page action `complete task '<Outcome>'`. It said "there is no `complete task`" (true only of microflows) and recommended buttons that call a microflow doing `set task outcome`. A new Task pages section gives a template. Its claim step is measured: on the 11.13.0 runtime a `complete task` button clicked by a targeted user who has not claimed the task fails with "You can't complete this user task, it is not assigned to you"; after an Assign-to-me microflow the same click completes it and the outcome branch runs. So the template has one shared claim microflow and one button per outcome. The microflow section now says where each form belongs, and the skill advises against a status attribute with a microflow per transition unless the prompt asks for one. The template builds clean on 11.15.0. Fixes #1709 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019xfGeNtrQ5BJrH2Ew64rt5
…outcomes (MDL-WF20) A task page is where a user finishes a task, and Mendix adds no buttons to it. A page that only shows the task passed check, mx check and the build, and every instance then waited at the task. An agent eval generated three such pages. MDL-WF20 warns per user or multi-user task, listing the outcomes its page cannot complete. An outcome counts as reachable through a `complete task` button or a button whose microflow reaches `set task outcome` (script or stored, through calls). Whatever the check cannot read stays silent: an unloadable microflow, a snippet, a page the script alters. The script pass runs without a project. Under -p, a stored page is read raw, with the key names taken from a dump of a real page, and only what the script pass missed is added. It is a warning, not a refusal: an outcome can be completed off the page on purpose. The syntax help, quick reference and write-workflows skill name the rule. Fixes #1710 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019xfGeNtrQ5BJrH2Ew64rt5
WorkflowCommons.MyInitiatedWorkflowView_WithdrawConfirmation now passes getput: its text area stores NumberOfLines 2, which the writer used to replace with 5 (#977 item 2). The allowlist may only shrink. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DRueZhZoBML5iWuJTvUnjL
Workflow eval priority-1 fixes: targeting XPath, interrupting path ends, task pages, MDL-WF20
CodeQL flagged the new NumberOfLines path: a value parsed with strconv.Atoi reached the writer's int32 conversion with no upper bound, so a large value would wrap. The builder and `alter page … set` now refuse a value above math.MaxInt32, and the writer converts only a value in range. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DRueZhZoBML5iWuJTvUnjL
CodeQL still saw the writer's int32 conversion as unbounded: the range check was on *n and the conversion a separate dereference, so the guard did not count. The value is now parsed with strconv.ParseInt(s, 10, 32) in the builder and in `alter page … set`, the model holds an int32, and the writer converts nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DRueZhZoBML5iWuJTvUnjL
A user task created with `targeting groups xpath [...]` was stored as Workflows$XPathGroupTargeting, but the reader had no case for that type and decoded it as no targeting. describe printed the task without the clause, so describe -> create or modify silently removed the targeting. Add the missing case. A table test round-trips every targeting kind the writer emits (single and multi user task), and an integration test replays the describe output with users-xpath targeting as the control; both fail with the fix reverted. Fixes #1722 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019xfGeNtrQ5BJrH2Ew64rt5
fix(workflows): read group XPath targeting back instead of dropping it
`[System.UserRoles = '[%UserRole_Manager%]']` with no Manager role passed
check --references and exec, and mx check then reported CE1613 at the
constraint. Worse, with any CE1613 in the app mx check reports no CE0161
at all, so one bad token hides every invalid XPath elsewhere.
Measured on mxbuild 11.15.0: an unknown role is CE1613 at a retrieve, a
list view's database source and an access rule alike, and the name is
matched case-sensitively ('UserRole_user' fails where 'UserRole_User' is
clean).
The reference pass now walks every XPath slot (retrieve, data source,
access rule, user task targeting as created and altered, offline sync)
and refuses a token whose role is neither in the project nor created by
the script, listing the roles and the right spelling for a case
mismatch. Excluded flows warn; without a project it stays silent.
Fixes #1723
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019xfGeNtrQ5BJrH2Ew64rt5
fix: carry a flow call's output mappings through a rewrite (#977 item 3)
fix(check): refuse an XPath [%UserRole_X%] token naming no user role
… state
describe -> create or modify on a workflow with Studio Pro-only settings
reported "Modified workflow" and reset them. Measured on 11.15.0 against
a stored workflow given each setting (mx check clean): the user task's
task name 'Review {1}' became its caption, the template parameter
{1} = $WorkflowContext/Name was dropped, both timer boundary event
captions were blanked, and a recurring reminder became a one-shot one.
REPLACE ACTIVITY rebuilt the activity the same way.
The rewrite guard now refuses, naming each setting and the issue that
adds its syntax: workflow display/description template parameters; a
restated user task's task name other than its caption, and its task
name/description parameters (#1714); a timer boundary event's caption
(#1715) and recurrence (#1716); and a boundary event annotation.
REPLACE ACTIVITY refuses the same for the activity it replaces. A task
the statement drops loses its name on purpose and is not refused.
Fixes #1713
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019xfGeNtrQ5BJrH2Ew64rt5
A call microflow / call nanoflow action can map what the flow returns back onto the page (Mendix 11.5, Studio Pro's "Return value mappings", Forms$OutputMapping). MDL had no syntax for them: describe printed nothing, and #1726 could only carry the stored ones through a rewrite. call microflow M.ACT_Vehicle_Get returns ( Brand = $ActionReturnValue/Brand, -- attribute of the result $stringResult = $ActionReturnValue, -- the result itself Brand = 'Demo brand') -- an expression A bare target is an attribute of the enclosing data container's object, $name a page variable. The three stored shapes match what Studio Pro 11.15 writes on ako/TestApp's Pages.OutputMappingButton (SourceVariable holds the target variable; the result itself fills no source slot). describe prints the list, a Studio Pro page round-trips (only the data view's FormValidations differs, which is #1729), and alter page set takes it. Floors: 11.5 (metamodel) for any mapping; 11.15 for an attribute target, measured: mx 11.13.0 and 11.14.0 report CE7006 on the same bytes 11.15.0-rc.4 accepts, before and after mx convert. Both are in the statement gate table, so check -p refuses what exec would. The carry is removed: with syntax, the statement is the authority, and the carry restored mappings a script had deliberately removed. A stored mapping the syntax cannot spell (a path over an association) is noted under the button instead. The MCP backend refuses mappings rather than dropping them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DRueZhZoBML5iWuJTvUnjL
fix(workflows): refuse a rewrite that would reset settings MDL cannot state
`boundary event interrupting timer <delay> caption 'Escalate' { … }` was
a parse error, so every timer boundary event mxcli wrote had an empty
caption in Studio Pro, and a describe -> create or modify blanked one
Studio Pro had set.
A timer now takes the `caption '…'` clause every activity takes, after
its delay, in CREATE and in ALTER's insert. The delay may be omitted, so
`caption` (and its alias `comment`) is excluded from the delay's first
token, as `non` already was. The caption reaches the writers through
BoundaryEvent.Caption and a caption argument on the workflow mutator's
InsertBoundaryEvent (wfmutator, MCP, mock), and describe prints it.
With that, the rewrite guard of #1713 no longer refuses a stored timer
caption.
Measured on 11.15.0: both captions stored as written, mx check 0
errors, describe prints them, replaying describe reports "Unchanged
workflow"; an ALTER insert with a caption stores it, 0 errors.
Fixes #1715
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019xfGeNtrQ5BJrH2Ew64rt5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DRueZhZoBML5iWuJTvUnjL
describe fragment printed `call microflow M.F` for `call microflow M.F(Order = $currentObject)`: flow and page arguments were dropped, as were a microflow/nanoflow data source's arguments. The same printer also wrote `create`, `openLink` and `param` for create object … then …, open link and a fragment action parameter, left out the fragment's own ($p: datasource, $q: action) header, and did not double a quote in a string property, so such output did not even parse. It now prints each as written. The test parses describe's output again and compares it with the fragment that was defined; before the fix it failed on every one of these. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DRueZhZoBML5iWuJTvUnjL
feat(workflows): give a timer boundary event a caption
…ame a task
`display 'Approval for {1}' with ({1} = $WorkflowContext/Title)` was a
parse error, and a user task's task name (what the task inbox shows) was
always its caption, so neither could be written, and a describe ->
create or modify reset a Studio Pro task name and its parameters.
The workflow's display and description, and a user task's new
`task name '…'` clause and its description, take `with ({1} = …)` like a
log message's template, in CREATE and in ALTER (`set (Display: …)`,
`set (TaskName: …) on <task>`). The writer stores each parameter as a
Microflows$TemplateParameter, the reader returns them, and describe
prints them (a task name only when it is not the caption). `name` is
read as an identifier after TASK, not made a keyword.
Measured with mx check on 11.15.0 (it parses these expressions): display
and description see $WorkflowContext only, a task's name and description
also $WorkflowInstance; $WorkflowUserTask and $currentUser are CE0117.
MDL-WF19 now checks these sites. A placeholder no parameter fills is
CE0720 and {0} CE0716: the new MDL-WF21 refuses both, and a parameter
numbering other than {1}..{n}. MCP refuses parameters rather than
writing the text without them. The #1713 rewrite refusal drops its
template and task-name rows.
End to end on 11.15.0: a workflow using every template builds with 0
errors, describe prints them, replaying describe reports "Unchanged
workflow"; the ALTER forms build clean too.
Fixes #1714
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019xfGeNtrQ5BJrH2Ew64rt5
…of a Protected module (#1699) A flow in an app module that ran `retrieve $l from AddOn.Internal`, `$o = create AddOn.Internal` or `$l = create list of AddOn.Internal` on a Hidden entity of a Protected (add-on) module passed `check -p --references` and exec, and mxbuild refused it with CE1406 "Retrieve object(s) activity / Create object activity / Create list activity … is not allowed to use entity 'AddOn.Internal' because app modules cannot reference hidden elements in add-on modules" (upstream mendixlabs#1652). Cause: validateProtectedModuleRefs judged an entity only in a flow's signature. checkFlowMembers judged the MEMBERS of a create or retrieve, never the entity itself, so an activity naming nothing but its entity reached no check. Fix: checkFlowMembers calls the existing entityRef for the entity of a create object, a database retrieve and a create list, in the walkTypedFlow visitor that already runs only for flows outside the Protected module. It is the one verdict check and exec's preflight both run, so exec now refuses the script before writing it. Verification: - Measured on Mendix 11.13.0 (module set Protected via UpdateModuleSettings, one microflow per shape): retrieve (list, limit 1), create (bare, with members) and create list of a Hidden entity are refused in microflows and nanoflows; the same against a Usable entity and inside the add-on are 0 errors; an association retrieve over a Usable association to a Hidden entity, and commit/delete/count of such an object, are not refused. - Tests in TestValidateProtectedModuleRefs, with Usable, in-module, ordinary module, script-made-usable and unknown-entity controls; reverting the fix fails the 8 refused cases with no error reported. - Real 11.13.0 app: the issue's script passed check (baseline) and mx check reported 3 CE1406; with the fix check reports the same 3 and exec refuses; with the hint's `alter entity … set export level api` check passes and mx check reports 0 errors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat: returns ( … ) syntax for flow-call output mappings (#977 item 3)
…te read on it passes (#1701) `$r = call microflow AddOn.Request_GetFirst(); declare $s String = $r/Secret;` in an app module passed `check -p --references` and exec while Secret was a Hidden attribute of a Protected (add-on) module, and mxbuild refused it with CE1406 "Create variable activity 'Create String variable' is not allowed to use attribute 'AddOn.Request.Secret' because app modules cannot reference hidden elements in add-on modules" (upstream mendixlabs#1654). The same read through a parameter was reported. Cause: walkTypedFlow, the one variable-typing walk the --references member checks share, typed parameters, create, retrieve and loop iterators but not the output variable of `call microflow`. `$r/Secret` had no entity, so both the Protected-module member check and the member-existence check were silent on every call result. Fix: walkTypedFlow takes the scriptContext validateProgram already builds and types a call's output from the callee's object or list return type (scriptContext.microflowReturnEntity): the script's own definition of the callee first, since a `create or modify` changes the signature exec writes, else the stored microflow, read by name and cached per pass. An unresolvable callee leaves the variable untyped. validateProtectedModuleRefs receives the sc. Same shared verdict for check and exec's preflight. Verification: - Measured on Mendix 11.13.0 (module set Protected, one microflow per shape): a declare, an end event and a change on a call result, and a change on a loop over a returned list, each CE1406 on the Hidden attribute; the Usable attribute and the same reads inside the add-on 0 errors. - Tests: TestValidateProtectedModuleRefs (object, list/loop, association, script-created callee, script rewrite of the return type; controls for a Usable attribute, in-module, ordinary module, non-object and unknown callee) and validate_call_result_typing_test.go for the existence check. Removing the typing fails 8 cases with nothing reported; disabling the script-first branch makes the existence check falsely report a member of the callee's NEW return type as missing. - Real 11.13.0 app: the issue's script passed check (baseline) and mx check reported 2 CE1406; with the fix check reports the same 2 and exec refuses; with the hint's `modify attribute … export level api` check passes and mx check reports 0 errors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…urce text (#1683) `log info node 'P' $Msg with ({1} = 'x');` logged the literal text `$Msg`, and `send email (… Subject: $Subj with ({1} = '42') …)` sent the subject `$Subj`. `show message` and `validation feedback` stored the same statement as '{1}' with the expression and then the stated parameters, so 'x' became a {2} no placeholder reads and the message showed its own unfilled {1}. check, exec and mx check (0 errors, 11.13.0) passed all four. Cause: a message template is a text with {n} placeholders; an expression has none for `with (…)` to fill, and Mendix cannot store an expression as a template's text. addLogMessageAction and emailTemplate (mdl/executor/cmd_microflows_builder_calls.go) fell back to fb.exprToString(message) as the template text when parameters were stated; addShowMessageAction / addValidationFeedbackAction appended them after the expression's {1}. Nothing in check refused the shape. Fix: one decision, messageTemplateRefusals (mdl/executor/message_templates.go), refuses an expression followed by `with (…)` (or the deprecated `objects [...]`) on all four statements — the shape MDL-REST05 and MDL-JA02 refuse for REST calls and Java action arguments. The builders call it (exec, for microflows, nanoflows, rules and alter fragments alike), and check reports it as MDL-TEMPLATE01 from the microflow, nanoflow, alter-fragment and rule validators. Sibling on the same branch: a multi-line string-literal email Body with parameters arrives as a SourceExpr and stored its source, quotes included, as the template; it is now stored as the literal's value. Verification: message_templates_test.go covers check and exec on every statement with controls (a string template, an expression without `with (…)`), every flow kind, and the multi-line body; reverting the production change fails all refused cases and the body case. On a blank 11.13.0 app the baseline wrote the four flows (describe printed '$Msg' / '$Subj'; mx check 0 errors); the fix refuses them at check and at exec --no-check, nothing written. The controls build at mx check 0 errors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat(workflows): fill workflow and task texts from expressions, and name a task
…oc (#1700) mxbuild writes a Java action's documentation as a javadoc block above `public class`. The file mxcli wrote never had it: a new action got none, and `create or modify java action` removed the one a build had written, so the next build put it back and every rewrite left a pair of unrelated VCS diffs. The model itself was intact. Mirror of mendixlabs#1653. Cause: GenerateSource (sdk/javaactions/source.go) took no documentation, and neither did backend.JavaBackend.WriteJavaSourceFile or its executor caller, although the executor had the documentation at hand. Fix: GenerateSource and WriteJavaSourceFile take the documentation; the executor passes ja.Documentation, the one the model ends up with, so a rewrite without a doc comment keeps the block. classJavadoc writes the block as measured on mxbuild 11.13.0 with documentation set straight in the model: `\r\n`, `\r` and `\n` each end a ` * ` line (so leading/trailing breaks give empty lines), whitespace is kept, whitespace-only documentation still gets a block, and a backslash is doubled unless `u`/`U` and four hex digits follow it. mxbuild refuses `*/` in the documentation as a model error. Mock, mcp and modelsdk stubs regenerated. Verification: 14 goldens from an mxbuild deploy build (testdata/mxbuild/JA_Doc*.java.golden, two of them generated from no file) are reproduced byte for byte; executor tests check the stated and the carried documentation reach the writer. Reverting the generator and the executor argument fails all of them except the no-documentation control. Real 11.13.0 app, 15 actions: after `create or modify` over mxbuild's files, 13 of 15 are byte-identical to mxbuild's with the fix (the other two differ only in the user-code padding mxcli has always written) against 1 of 15 with the baseline; the next mxbuild build rewrote 0 of 15 files after the fix and 14 of 15 after the baseline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Studio Pro's recurring reminder had no MDL spelling: every non-interrupting timer mxcli wrote fired once, and a describe -> create or modify turned a Studio Pro reminder into a one-shot timer. `repeat every <n> <unit> at most <m> times` now follows a non-interrupting timer's delay — a boundary event, in CREATE and in ALTER's insert — and a timer event sub-process start's expression. It is written as Workflows$LinearRecurrence, read back, and printed by describe. repeat, every and times are keywords that stay usable as names; `repeat` is excluded from a delay's first token. The values come from mxbuild 11.15.0 itself: the runtime's scheduled-event interval enum (Second..Year as stored), and Studio Pro's model validation, which allows minute, hour, day, week or month (MW0005) and at least 2 executions counting the first (MW0004). mx check enforces none of it, so MDL-WF22 refuses an interrupting timer, any other unit, an interval below 1 and fewer than 2 executions. Measured: 0 errors on 11.15.0, describe -> replay "Unchanged workflow", an ALTER insert stores a weekly one. At runtime (every minute, at most 3 / 2 times) the boundary timer fired 3 times on 11.13 and on 11.15; the event sub-process start fired twice on 11.15 but once on 11.13, whose runtime model has no recurrence on the start, so that form is gated at 11.15. MCP refuses a recurrence rather than writing a one-shot timer. The #1713 rewrite refusal drops its recurrence row. Fixes #1716 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019xfGeNtrQ5BJrH2Ew64rt5
An action whose flow, page or entity is not selected yet was described as a bare `call nanoflow`, `call microflow`, `show page` or `create object`. None of these parses, so one unwired button made the whole page's description unusable. ako/TestApp's Pages.OutputMappingButton failed with "extraneous input ')'". Studio Pro stores this state for placeholders: 16 in ako/TestApp and 5 in PedApp, all in page templates except that one page, which inherits them when made from a template. mx check rejects it on a page (CE0571 no nanoflow or microflow, CE0566 no page, CE0460 no entity), so it gets a NOT re-executable note on its own line rather than new syntax. A re-run writes no action there. Navigation's menu items keep their existing message, and the rewrite keeps the stored action as before. Named action slots and object-list item actions printed the rendered action as `key: value`, which a note would have broken; they now go through actionProp like Action and OnClick. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DRueZhZoBML5iWuJTvUnjL
feat(workflows): repeat a non-interrupting timer
#1702) Studio Pro keeps three parts of a Java action's .java file when it regenerates it: the import list, the USER CODE and the EXTRA CODE section. MDL could author the first two (an import line in the body is hoisted to the import list) but had no clause for the third, and `describe java action` printed only the body. `create or modify` kept a hand-written helper in place, but executing the describe output on another project wrote a body calling a helper that no longer existed, and the build failed. Mirror of mendixlabs#1658. Cause: createJavaActionStatement ended at `AS $$ … $$` (the AST's ExtraCode field was never set), and describeJavaAction read only the USER CODE section. Fix: create or modify java action ModJ.JA_Twice(N: Integer not null) returns Integer as $$ import java.util.Objects; return twice(N); $$ extra code $$ private static long twice(long n) { return Objects.requireNonNull(n) * 2L; } $$; `extra code $$ … $$` (new EXTRA/CODE keywords, both still names) states the section, kept as written apart from its blank edge lines; an import line in it goes to the import list too. Omitted on `create or modify`, the stored section is kept, as an omitted EXPOSED AS keeps the toolbox entry; `extra code $$ $$` clears it, so WriteJavaSourceFile/RetainSections take the section as a *string (nil keeps). describe prints the section and, at the top of the body, the shortest leading part of the stored import list that regenerates it (javaactions.StatedImports) — nothing for mxbuild's own list. Verification: visitor, sdk and executor tests, including describe → exec of the report's file rebuilding it byte for byte and the omitted-clause control handing the writer nil; stubbing the visitor, describe and RetainSections changes fails them. Real 11.13.0 app with the report's steps: the baseline replay on a fresh project lost the import and helper and mxbuild failed with `cannot find symbol`; the fix's replay is byte-identical to the source file, builds, and the build leaves it unchanged. `create or modify` without the clause leaves the file unchanged. 07-java-action-examples.mdl passes exec + mx check with the new example. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A microflow data source stores its call settings in a Forms$MicroflowSettings, as a call-microflow action does, but the writer always wrote that data source's FormValidations as All. ako/TestApp's Pages.OutputMappingButton has a data view whose source stores None: describe → exec rewrote it as All and reported "Replaced page", and that was the only difference on the page. A microflow data source now takes `with (FormValidations: All|Widget| None)`, the spelling an action already has. It is written, described when it is not All, and settable through alter page set DataSource. It is the only setting a data source takes. Across 44 TestApp and 4 PedApp sources every other setting is at its default, so the rest are refused on a data source as an action's. The action path already kept a stored None. With #1728, the TestApp page now round-trips as "Unchanged page" once its unwired nanoflow button has a nanoflow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DRueZhZoBML5iWuJTvUnjL
engalar
pushed a commit
to engalar/mxcli
that referenced
this pull request
Oct 27, 2026
…3q3t fix: alter entity add event handler adds a second handler for the same event
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Syncs
ako/mxcli:maininto upstream: 40 commits from 18 fork PRs, ako#1696–ako#1737.Fixes #1644, #1652, #1653, #1654 and #1658. Refs #1517.
Microflow splices: the open #1517 decisions
The previous sync left three items of #1517 waiting on a decision. They are implemented here:
alter … replace body of.alter … insertproduces can be dropped or replaced. Re-running the original script after another script spliced a decision in no longer fails.@annotation(id:).Workflows
display 'Review {1}' with ({1} = …). A task can be named.[System.User/Name = 'MxAdmin']used to pass check.targeting groups xpathis read back. Describe used to drop it.create or modify workflowrefuses a rewrite that would reset settings MDL can't state.check
check -p --referencesmisses CE1406 for a database retrieve or a create of a Hidden entity of a Protected module #1652), and for a Hidden attribute read on acall microflowresult (check -p --referencesdoes not type acall microflowresult, so a Hidden attribute of a Protected module read on it passes (CE1406) #1654). check didn't type that result.[%UserRole_X%]token that names no user role is refused. It used to pass check, then fail mx check.Java actions (#1653, #1658)
create java actionwrites the.javafile with the class javadoc (create [or modify] java actionwrites the .java file without the class javadoc that Studio Pro and mxbuild generate from the documentation #1653).extra codeclause forcreate java action(create java actionhas no clause for the EXTRA CODE section anddescribe java actionomits it: a describe round trip loses hand-written helpers (create or modify keeps them in place) #1658). Describe prints it, so the EXTRA CODE section round-trips.Pages: input widgets and round trips (#1644)
ReadOnlyStyleis carried on text box, text area, date picker and radio buttons.NumberOfLinesandAutocompleteround-trip.NumberOfLinesis bounded to the stored 32-bit range.customDateFormatis unescaped and re-escaped.SETthat would leave a date picker on Custom with no pattern is refused.returns ( … )syntax.Action: call nanoflow), which didn't parse.Microflow templates
logandsend emailwith an expression message followed bywith (…)no longer store the expression as its source text.describe fragmentprints arguments and the whole action.