Skip to content

Sync ako/mxcli: loop-body splices and #1517 decisions, workflow texts and timers, Java action round trips, input widget drops - #1660

Merged
ako merged 59 commits into
mendixlabs:mainfrom
ako:main
Oct 27, 2026
Merged

ako merged 59 commits into
mendixlabs:mainfrom
ako:main

Conversation

@ako

@ako ako commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator

Syncs ako/mxcli:main into 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:

  • Plain activities inside a loop body are spliced. A structural change inside a loop that can't be spliced now has an explicit fallback, alter … replace body of.
  • A decision of the shape alter … insert produces can be dropped or replaced. Re-running the original script after another script spliced a decision in no longer fails.
  • An alter fragment's statement attaches to a stored note by its text, not by a describe-time @annotation(id:).

Workflows

  • New syntax:
    • Workflow and user-task names and descriptions take parameters: display 'Review {1}' with ({1} = …). A task can be named.
    • A timer boundary event takes a caption.
    • A non-interrupting timer can repeat, which is Studio Pro's repeating reminder.
  • Fixes:
    • User task targeting XPath is checked against the user entity. [System.User/Name = 'MxAdmin'] used to pass check.
    • targeting groups xpath is read back. Describe used to drop it.
    • An interrupting boundary path ends with an End, as Studio Pro does.
    • create or modify workflow refuses a rewrite that would reset settings MDL can't state.
  • New warning: MDL-WF20 warns when a user task's page can't complete one of its outcomes. The write-workflows skill now gives task pages a claim button and one complete-task button per outcome.

check

Java actions (#1653, #1658)

Pages: input widgets and round trips (#1644)

  • A tab page's Badge has an MDL spelling (Tab page Badge has no MDL spelling: describe omits it and a describe round trip removes it #1644), so a rewrite keeps it.
  • Input widget settings that rewrites used to drop:
    • ReadOnlyStyle is carried on text box, text area, date picker and radio buttons.
    • A text area's NumberOfLines and Autocomplete round-trip. NumberOfLines is bounded to the stored 32-bit range.
    • A dynamic text's customDateFormat is unescaped and re-escaped.
    • A SET that would leave a date picker on Custom with no pattern is refused.
  • Flow-call output mappings:
    • New: returns ( … ) syntax.
    • A rewrite carries them.
  • describe → exec keeps the stored order of placeholders and translations. Six documents now round-trip and are struck from the allowlists.
  • Two describe gaps are closed:
    • An action with no target selected is described as a note, instead of Action: call nanoflow), which didn't parse.
    • A microflow data source keeps its FormValidations.

Microflow templates

  • log and send email with an expression message followed by with (…) no longer store the expression as its source text.
  • describe fragment prints arguments and the whole action.

ako and others added 30 commits October 25, 2026 18:23
…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)
…ions

feat: splice plain activities inside loop bodies, alter … replace body of, and drop an inserted decision (#1693, #1694)
…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
claude and others added 27 commits October 26, 2026 18:52
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
fix: describe unset action targets as notes; keep a data source's FormValidations (#1728, #1729)
fix: check misses CE1406 for Hidden entities of Protected modules and on call results (#1699, #1701)
…dtrip

fix: templates with an expression and with(…); Java action javadoc and EXTRA CODE round trip (#1683, #1700, #1702)
@ako
ako merged commit 2be9331 into mendixlabs:main Oct 27, 2026
15 checks passed
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tab page Badge has no MDL spelling: describe omits it and a describe round trip removes it

2 participants