Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
13accfc
docs(wiki): re-sync describe-round-trip-gaps after #923 closed out
claude Sep 14, 2026
60ff7f7
docs(plans): removal plan for the legacy engine
claude Sep 14, 2026
9ee0bcc
feat(workflow): author event handlers and on-created microflows
ako Sep 14, 2026
79e56c7
Merge pull request #467 from ako/docs/workflow-studio-pro-constructs
ako Sep 15, 2026
5a05766
Merge branch 'mendixlabs:main' into main
ako Sep 15, 2026
e0b4036
docs(plans): measure Phase 3 — it is a bypass problem, not a serializ…
claude Sep 15, 2026
5b600cb
Merge remote-tracking branch 'origin/main' into claude/mxcli-unit-tes…
claude Sep 15, 2026
9b39ec6
docs(plans): refresh the plan's measurements against current main
claude Sep 15, 2026
28ae3ea
feat(workflow): author AI agent tasks with `call agent microflow`
ako Sep 15, 2026
3b4df7c
refactor(api): route the fluent API through the backend abstraction
claude Sep 15, 2026
60bfd35
Point the MCP backend's local reads at the codec backend
claude Sep 15, 2026
5feb4b8
fix(layout): enforce the placeholder rule mxbuild actually applies
claude Sep 15, 2026
ba2c845
fix(layout): report a braced placeholder instead of dropping it silently
claude Sep 15, 2026
284fa6b
feat(layout): add DROP LAYOUT
claude Sep 15, 2026
884a2bf
docs(layout): correct the claim that `Main` is a naming convention
claude Sep 15, 2026
3d3ca1f
Delete the legacy sdk/mpr backend and the flag selecting it
claude Sep 15, 2026
8ad6584
Merge pull request #468 from ako/claude/mxcli-unit-test-perf-n7ggx8
ako Sep 15, 2026
7c7ae8b
Merge origin/main (#468: legacy engine retired, MCP reads via the cod…
ako Sep 15, 2026
de4b09e
docs(proposal): phase 2 engines paragraph after the legacy engine's r…
ako Sep 15, 2026
f06a855
Delete the dead FullBackend surface the census had mislabelled
claude Sep 15, 2026
15bfb20
Merge pull request #469 from ako/feat/workflow-ai-agent-task
ako Sep 15, 2026
44cbdc4
Merge origin/main into claude/mxcli-findings-nnl181
claude Sep 15, 2026
694b379
test(mcp): dial the fake PED at its listener, not through defaultDial
ako Sep 15, 2026
f990ff2
Merge origin/main (#469: AI agent tasks in workflows) into the dead-s…
claude Sep 15, 2026
898e78e
feat(workflow): multi-user task completion rules — participants, deci…
ako Sep 15, 2026
f9d2525
Merge origin/main (#469 merged) into feat/workflow-completion-rules
ako Sep 15, 2026
76fa138
Close Phase 3: accept the last six bypasses as deliberate
claude Sep 15, 2026
49beada
Re-measure Phase 4: it is two independent migrations, not one
claude Sep 15, 2026
03a3ef9
Merge pull request #470 from ako/claude/mxcli-findings-nnl181
ako Sep 15, 2026
7440631
Cut the root package off sdk/mpr (Phase 4a gate)
claude Sep 15, 2026
846d335
Merge origin/main (#470: layout placeholder rule, DROP LAYOUT) into t…
claude Sep 15, 2026
ccd92a2
feat(workflow): event sub-processes and notification events
ako Sep 15, 2026
353d5fd
Merge pull request #471 from ako/claude/mxcli-unit-test-perf-n7ggx8
ako Sep 15, 2026
9a3e3eb
Merge branch 'main' into fix/mcp-connect-tests-dial
ako Sep 15, 2026
ffa439d
Merge pull request #472 from ako/fix/mcp-connect-tests-dial
ako Sep 15, 2026
725f188
Merge branch 'main' into feat/workflow-completion-rules
ako Sep 15, 2026
7a9a6d6
Merge pull request #473 from ako/feat/workflow-completion-rules
ako Sep 15, 2026
90fb7c6
Port cmd/mxcli's readers off sdk/mpr (Phase 4a, first slice)
claude Sep 15, 2026
648e346
Merge pull request #475 from ako/feat/workflow-event-subprocesses
ako Sep 15, 2026
b071fcb
feat(workflow): notify workflow … target, and fix notify actions that…
ako Sep 15, 2026
e19ba2f
Merge origin/main (#475: workflow event sub-processes) into the Phase…
claude Sep 15, 2026
0a1490b
Merge branch 'main' into feat/workflow-notify-target
ako Sep 15, 2026
997af5b
Merge pull request #476 from ako/feat/workflow-notify-target
ako Sep 15, 2026
038f810
Merge pull request #477 from ako/claude/mxcli-unit-test-perf-n7ggx8
ako Sep 15, 2026
692bafa
fix(workflow): resolve ALTER WORKFLOW activity refs by name before ca…
ako Sep 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .claude/skills/fix-issue/findings/mdl-backend.jsonl

Large diffs are not rendered by default.

13 changes: 10 additions & 3 deletions .claude/skills/fix-issue/findings/mdl-executor.jsonl

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions .claude/skills/fix-issue/findings/mdl-visitor.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@
{"area": "mdl/visitor", "symptom": "`Height: n` on a domain-model annotation is a bare parse error (\"mismatched input 'Height' expecting {POSITION, CAPTION, WIDTH}\"), which reads like a missing mxcli feature and gets reported as one. Filed as a feature request to expose annotation height.", "cause": "Not an mxcli gap: Mendix stores no annotation height. DomainModels$Annotation has exactly Caption, ExportLevel, Location and Width, so the note auto-sizes to its caption and there is nowhere to write a height. Adding the property would mean inventing a key the platform does not have.", "file": "`mdl/visitor/visitor.go` (`looksLikeAnnotationProperty`, `annotationHeightRe`, the two branches in `enhanceErrorMessage`); syntax topic `cmd/mxcli/syntax/features_domain_model.go`; type doc `mdl/ast/ast_annotation.go`; tests `mdl/visitor/annotation_height_hint_test.go`.", "insight": "`generated/metamodel` alone CANNOT settle \"does this property exist\" — it is an 11.6.0 snapshot, and the report was against 11.12.2, so a property added later would be invisible to it. Four sources were needed and three of them are cheap: `mx dump-mpr` (Mendix's own serializer) emits the four keys on real projects; `mx convert -p` is the decisive one, because it rewrites the model through Mendix's OWN object model and would materialise a property that merely had a default and was not stored — it added nothing; and the published Model SDK's domainmodels.Annotation lists caption/exportLevel/location/width. An absent key in stored BSON proves nothing on its own (it may just be unwritten), which is why dump-mpr alone is not enough. When a feature cannot exist, the deliverable is a parse error that SAYS so and names the levers that do exist (Width changes the wrapping and therefore the height; `ALTER ENTITY … SET POSITION` moves what the note overlaps) — the reporter's real goal was reachable already. Key the hint on the `expecting {POSITION, CAPTION, WIDTH}` token set, not on the word \"Height\": a page widget's Height is valid MDL, and a name-keyed hint would misfire on it (control: TestWidgetHeightIsNotAnnotationHinted).", "refs": ["#1014"], "date": "2026-09-01"}
{"area": "mdl/visitor", "date": "2026-09-08", "symptom": "`call microflow M.ACT with (Ctx = $WorkflowContext)` — an UNQUOTED value in a workflow parameter mapping — crashed the binary with SIGSEGV (nil pointer) in buildWorkflowCallMicroflow, on `check`, `check --references` and `exec` alike, with no diagnostic beyond the Go panic. Reported as mendixlabs/mxcli#1023.", "cause": "The grammar rule workflowParameterMapping requires STRING_LITERAL, but visitor.Build() walks the parse tree even when the parse FAILED (deliberately — that is what lets check report more than the first error). Under ANTLR error recovery the rule context exists with a nil STRING_LITERAL child, and the visitor read it unguarded. Same bug at the CALL WORKFLOW site.", "fix": "Factor both sites into buildWorkflowParameterMappings, nil-checking QualifiedName() and STRING_LITERAL() and skipping the mapping. The syntax error the listener already recorded ('mismatched input ... expecting STRING_LITERAL') becomes what the author sees.", "file": "mdl/visitor/visitor_workflow.go", "insight": "In this codebase a required grammar child is NOT a guarantee inside the visitor, because Build() walks a failed parse on purpose. Every ctx.X().GetText() on a required child is therefore a latent crash reachable from ordinary malformed input — grep 'STRING_LITERAL().GetText()' for the ones still unguarded. The other half of the finding is documentation-shaped: `mxcli syntax workflow call-microflow` omitted the WITH clause entirely, so an author had nothing to copy and reached for the bare-variable spelling used everywhere else in MDL. A crash on input the tool's own docs do not cover is a docs bug with a segfault attached."}
{"area": "mdl/visitor", "date": "2026-09-07", "symptom": "`create or modify snippet M.S (params: { $T: Mod.\"Thing\" })` failed at execution with `failed to resolve entity Mod.\"Thing\": entity not found`, while the identical quoted form in a PAGE parameter resolved fine (ako/CapTrackV4 019).", "cause": "buildSnippetParameterListAsPage re-split the parse node's TEXT (`parseQualifiedName(dt.GetText())`), and GetText() returns the source verbatim, quotes included. The page path walks the parse tree, where buildQualifiedName unquotes each part via identifierOrKeywordText. Fixed by walking the tree; the dead duplicate buildSnippetParameters — a correct implementation nothing called — was removed.", "file": "`mdl/visitor/visitor_page_v3.go` (buildSnippetParameterListAsPage); `mdl/visitor/visitor_page.go` (removed buildSnippetParameters); tests `mdl/visitor/snippet_param_quoted_entity_test.go`", "insight": "GetText() on an ANTLR context is the source text, not the resolved value, so any conversion built on it silently keeps quoting, whitespace and casing that the tree-walking helpers strip. Grep for `parseQualifiedName(.*GetText())` when a name resolves in one statement and not in a sibling. The asymmetry is also the diagnosis: when two statements accept the same syntax and only one works, compare their VISITORS before their executors — here both executor paths were identical and called the same resolveEntity. Two copies of one conversion with one of them dead is how they drifted, so the dead one is deleted rather than fixed."}
{"area": "mdl/visitor", "date": "2026-09-15", "symptom": "`placeholder Main { }` inside a CREATE LAYOUT passes `mxcli check`, then fails at exec with `layout \"X\" declares no placeholder` \u2014 a message that flatly contradicts the script, which says `placeholder Main`. The failed exec has already created the module.", "cause": "One grammar rule (placeholderBlockV3) serves two opposite jobs, told apart by shape: `if c.LBRACE() == nil` makes a bodiless placeholder a DECLARATION widget, and the braced form is routed to buildPagePlaceholdersV3 \u2014 the page-side job of FILLING a layout slot. In a layout there is no such job, so the braced form was dropped on the floor and the layout ended up with zero placeholders.", "fix": "Builder gained `inLayout` (saved/restored around the layout body build, since the same body builder serves pages) and collects the dropped names into ast.CreateLayoutStmt.BracedPlaceholders; MDL083 reports them at check time. The braced form is still dropped \u2014 recording it is a diagnostic, not a decision to honour it.", "insight": "When one parse rule serves two documents and is disambiguated by SHAPE rather than by context, the wrong shape has no error path by construction \u2014 it just silently means the other thing. The tell is a runtime message that contradicts the source text. Note the mistake is the natural one: every other layout element takes a body, and `alter page` uses the braced form for real, so the author is generalising correctly from the rest of the language. DESCRIBE emits the bodiless form, so round-tripping never produces it and no existing test covered it.", "controls": "A page's braced placeholder must still fill a slot (TestBuildPageV3_BracedPlaceholderStillFillsASlot) and a layout's bodiless form must still produce a real widget \u2014 the flag is save/restored precisely so a page later in the same script is not flagged.", "refs": "mendixlabs/mxcli#1063", "file": "mdl/visitor/visitor_page_v3.go, mdl/visitor/visitor.go, mdl/ast/ast_page_v3.go"}
1 change: 1 addition & 0 deletions .claude/skills/fix-issue/findings/sdk.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@
{"area": "sdk", "date": "2026-09-04", "symptom": "REPORTED AS A BUG, MEASURED AS A NON-BUG. `create association \u2026 type ReferenceSet owner Both` without `STORAGE TABLE` writes `StorageFormat: \"Column\"`, which was reported as \"not a legal many-to-many\" and worked around by respelling every such association.", "cause": "Nothing is broken. Measured on Mendix 11.13 against a live PostgreSQL, with the two spellings side by side in one app: `App.PA_PB` (ReferenceSet, StorageFormat Column) and `App.PC_PD` (ReferenceSet, StorageFormat Table) produce IDENTICAL DDL \u2014 `app$pa_pb(app$paid, app$pbid)` and `app$pc_pd(app$pcid, app$pdid)`, two FK constraints each. The app boots and serves HTTP 200, and `mx check` reports 0 errors. Mendix ignores StorageFormat for a reference set and always uses a junction table.", "file": "no code change \u2014 `mdl/executor/cmd_associations.go` defaults storageFormat to Column for every association type, and that is harmless", "insight": "A reported bug is a symptom plus an EXPLANATION, and the explanation is the part to re-measure. \"mxcli writes Column\" was true; \"which is not a legal many-to-many\" was the inference, and it cost the reporter a rewrite of ten associations. The cheap discriminator was to author BOTH spellings in one app and compare the DDL the runtime actually creates \u2014 a side-by-side control in the same boot, rather than reasoning about what a column could hold. Note the pkill trap from the same FINDINGS (\u00a711) applies when tidying up afterwards: `pkill -f \"mxcli run\"` matches the calling shell and kills it (exit 144)."}
{"area": "sdk/mpr", "date": "2026-09-06", "symptom": "`mx check` reported CE0066 \"Entity access is out of date\" at \"Domain model of module 'BusinessEvents'\" after `create or modify persistent entity BusinessEvents.PublishedBusinessEvent ( EventId: long )` over the real BusinessEvents 3.12.0 marketplace module. LEGACY ENGINE ONLY — the codec engine produced 0 errors from the same script. Caught by the integration gate (TestMxCheck_DoctypeScripts/13-business-events-examples.mdl/legacy), not by any unit test.", "cause": "ReconcileMemberAccesses in sdk/mpr/writer_security.go skipped any rule whose MemberAccesses list held only the storage marker (`if len(maArr) <= 1 { break }`), so it never topped one up. A rule with zero member entries on an entity that HAS members is precisely the out-of-date state CE0066 names, so the skip left behind the one thing the function exists to prevent. Nothing reached that state until `create or modify entity` started PRESERVING access rules instead of deleting them: the rewrite dropped all five attributes the Administrator rule covered, the prune emptied the list, and the new EventId then never got an entry. Fixed by narrowing the guard to `len(maArr) == 0` (no storage marker at all).", "file": "`sdk/mpr/writer_security.go` (ReconcileMemberAccesses, the MemberAccesses loop); tests `sdk/mpr/writer_security_reconcile_test.go`", "insight": "A fix that starts PRESERVING something reaches states no prior code could produce, so its blast radius is every consumer of that thing — here a reconcile function untouched for months. The engine split is the tell worth acting on: identical script, 0 errors on modelsdk and CE0066 on legacy, which localises the defect to the legacy path in one measurement and makes the codec engine the reference for what the document should contain (dumped both: 1 member entry vs 0). Also note where this was caught — only the integration gate exercises a real marketplace module, and only that module had a rule whose entire member set the script drops. The unit tests written for the entity fix were green throughout, and were right to be: the entity layer did exactly what it should. Keep the empty-list case as a named test on both sides, with a member-less entity as the control, since the old guard covered that case by accident and removing it must not turn every member-less entity into a write."}
{"area": "sdk/mpr", "date": "2026-09-12", "symptom": "The legacy writer's image widgets disagree with Studio Pro. `serializeStaticImage` omits AlternativeText entirely; `serializeDynamicImage` writes one containing a `FallbackValue` string; both write BSON null for the unset Image / DefaultImage. mxbuild accepts all of it at 0 errors", "cause": "`Forms$ClientTemplate` has exactly three properties — Fallback (Texts$Text), Parameters, Template (generated/metamodel, and all three Studio Pro references). The dynamic image hand-rolled its own holder instead of calling `serializeClientTemplate`, and invented FallbackValue. AlternativeText is declared without omitempty on both image types and appears in 3/3 references, so omitting it is a drop, not an optional key", "file": "`sdk/mpr/writer_widgets_display.go` (serializeStaticImage, serializeDynamicImage, emptyAlternativeText)", "insight": "**A hand-rolled copy of a shared serializer is where the invented key lives.** The correct helper was four lines away and carried a comment naming this exact mistake; the copy still got it wrong, because nothing compares the two. Grep for a type's $Type string and check whether every construction site goes through one builder. **mxbuild is not a check for this class at all** — it tolerates unknown properties, while Studio Pro resolves every stored property against the type's property list and throws \"Sequence contains no matching element\" at MprProperty.cs. The available substitutes are generated/metamodel (the arbiter) and a real Studio Pro document from a marketplace module in the fixture", "refs": []}
{"area": "sdk/mpr", "date": "2026-09-14", "symptom": "Legacy engine: a user task's on-created microflow (set in Studio Pro) reads back as empty — `describe workflow` omits it and the semantic `UserTask.OnCreated` is \"\" — while the modelsdk engine reads it", "cause": "`parseUserTask` did `raw[\"OnCreatedEvent\"].(string)`, but the stored value is a PART document: `{ $Type: Workflows$MicroflowBasedEvent, Microflow: \"Mod.MF\" }` or `{ $Type: Workflows$NoEvent }`. The type assertion never matched, silently", "file": "`sdk/mpr/parser_workflow.go` (`parseUserTask`)", "insight": "A `.(string)` assertion on a key whose metamodel type is a part/by-name-in-a-part fails silently and yields the zero value — indistinguishable from 'not set'. When a field reads empty on one engine only, check the stored shape with a Studio Pro reference document (ako/TestApp) before assuming the model lacks it. The test round-trips the writer's own document through `bson.Marshal`/`Unmarshal` so the parser sees real decoded types; control: restoring the string assertion fails it"}
7 changes: 2 additions & 5 deletions .claude/skills/mendix/atlas-design/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,14 +185,11 @@ alter page Sales.CustomerOverview set cust_text22 (content: 'Customers');
> `CATALOG.building_blocks`) **and** instantiation (`USE BUILDING BLOCK`) both work
> today. `use building block` v1 is **deep-copy + optional `as <prefix>`**; configure
> the copy afterwards with `alter page` (an inline override block is a proposed v1.1).
> It runs on `MXCLI_ENGINE=legacy` today; modelsdk-engine support lands with that
> engine's `ListBuildingBlocks`.

**When to *mirror* instead.** *Mirroring* — reproducing a block's tree by hand with
`create page`/`alter page` + the same classes and design properties (see below) — is
the fallback: reach for it only to hand-tune a shape Atlas doesn't quite give you, or
on the modelsdk engine before its building-block support lands. Otherwise prefer the
one-line `use building block`.
the fallback: reach for it only to hand-tune a shape Atlas doesn't quite give you.
Otherwise prefer the one-line `use building block`.

---

Expand Down
2 changes: 0 additions & 2 deletions .claude/skills/mendix/manage-navigation/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -442,8 +442,6 @@ export level are preserved, so menu widgets pointing at it keep working.
image icon cannot be written by MDL; `describe` flags those on their own
comment line rather than dropping them silently, so re-running the output
loses that icon visibly.
- **Authoring needs the default engine.** Under `MXCLI_ENGINE=legacy`,
create/modify/drop refuse rather than writing a differently-shaped document.

## Offline Profiles

Expand Down
49 changes: 44 additions & 5 deletions .claude/skills/mendix/write-layouts/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,26 @@ before running it** — the comments name exactly what will be lost. To change a
layout without that risk, use `ALTER LAYOUT`, which edits the stored document in
place and leaves everything it was not asked to touch alone.

### Removing a layout

```sql
DROP LAYOUT MyModule.App_Old;
```

Pages still bound to it are **named in a warning and the drop proceeds** — it is
not refused. Left dropped, each of those pages fails the build with **CE1613**
("The selected layout … no longer exists"), which names the *page* and never the
layout, so repoint them first:

```sql
ALTER PAGES SET LAYOUT = MyModule.App_New WHERE LAYOUT = MyModule.App_Old;
DROP LAYOUT MyModule.App_Old;
```

To *correct* a layout rather than remove it, re-create it under the same name
(`CREATE OR REPLACE LAYOUT`): the pages stay bound by qualified name and rebind
to the new document — verified end to end, the pages go back to 0 errors.

### Repointing pages

A new layout that no page uses changes nothing:
Expand Down Expand Up @@ -171,17 +191,36 @@ section.
- **A placeholder's name is API.** A page binds to it as
`Module.Layout.<Name>`, stored as a qualified name. Renaming one unbinds every
page that used it — the page still builds, and its content vanishes.
- **Name one placeholder `Main`.** `Forms$Layout` has no property saying which
placeholder is the main one; the convention is the mechanism, and 22 of 22
Atlas layouts follow it.
- **Exactly one placeholder must be named `Main` — this is a rule, not a
convention.** `Forms$Layout` has no property saying which placeholder is the
main one, but mxbuild validates the NAME. Measured on 11.12.1 against a layout
**no page uses**, so none of it depends on a page binding:

| Declares | mxbuild |
|---|---|
| `Main` | 0 errors |
| `Main` + `Content` | 0 errors — extra names are fine |
| `Content` only | **CE0848** "No placeholder with the name 'Main' found. There should be exactly one." |
| `Main` + `Main` | **CE0849** + CE0495 |
| `Main` + `Side` + `Side` | **CE0495** "Duplicate name 'Side'." — uniqueness is general |

`mxcli check` reports these as **MDL081** (the Main rule) and **MDL082**
(duplicate names). Earlier versions said this was a convention and checked
only that *some* placeholder existed, so a layout naming it anything else
passed `check` and `exec` and failed the build (mendixlabs/mxcli#1063).
- **There is no `mainplaceholder:` property, on purpose.** `modelsdk/gen`
declares `MainPlaceholderName` on `Layout` so the setter compiles, and mxbuild
accepts the result — measured 0 errors. But `generated/metamodel` does not
declare it and no Studio Pro layout carries it, and Studio Pro resolves every
stored property against the type's list. Writing it gives you a layout that
builds and cannot be opened.
- **A layout must declare at least one placeholder.** Otherwise no page can use
it. Refused at write time.
- **A placeholder is declared with NO body.** `placeholder Main { … }` is the
page-side spelling — in a page it *fills* a layout's slot; in a layout it
declares nothing and is dropped, leaving the layout with no placeholder at all.
Reaching for it here is the natural mistake, since every other layout element
takes a body. Reported as **MDL083**.
- **A layout that declares no placeholder is refused at write time** as well as
at check time — no page could use it.
- **The sidebar toggle, the menu bar's logo and Atlas's `Forms$Header` are not
authorable.** A topbar layout that needs a collapsible sidebar therefore has to
keep Atlas's, or do without the toggle — which is why `mxcli new` scaffolds a
Expand Down
Loading
Loading