Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
60123e2
docs(soap): measure the proposed syntax against the sibling call stat…
claude Sep 11, 2026
7261dfd
feat(soap): make the request body authorable — arguments and send map…
claude Sep 11, 2026
aaaf5b2
fix(alter-page): resolve every data source kind in one walk
claude Sep 11, 2026
566911b
Merge remote-tracking branch 'origin/main' into claude/gallant-curie-…
claude Sep 11, 2026
6fec7ad
docs(microflow): audit what a rewrite loses, across 342 microflows
claude Sep 11, 2026
953d459
Merge pull request #446 from ako/claude/gallant-curie-4vivo0
ako Sep 11, 2026
2a9d3f1
Merge branch 'mendixlabs:main' into main
ako Sep 11, 2026
8d97e43
fix(microflow): stop a rewrite clearing "apply entity access" and "bl…
claude Sep 11, 2026
4ed9cd4
fix(mdl): MDL059 now covers annotations written before a CREATE
claude Sep 12, 2026
dd50f10
Make `Action: NOTHING` a real action expression
claude Sep 12, 2026
56f95e7
Report a non-action in a widget's action slot (MDL-WIDGET28)
claude Sep 12, 2026
8a38d07
Merge remote-tracking branch 'origin/main' into claude/mendix-default…
claude Sep 12, 2026
565d7dc
Merge pull request #447 from ako/claude/mendix-default-styling-1fgvpm
ako Sep 12, 2026
141da93
fix(pages): close the last modelsdk widget gaps, refuse the one that …
claude Sep 12, 2026
a139d38
feat(modelsdk): implement the only two reachable unimplemented backen…
claude Sep 12, 2026
d4bb899
fix(check): renumber the statictext rule, and stop legacy running on …
claude Sep 12, 2026
e1b4719
Merge remote-tracking branch 'origin/main' into claude/mxcli-unit-tes…
claude Sep 12, 2026
f75aa4a
docs(wiki): re-synthesize check-mxbuild-drift and widget-type-object-…
ako Sep 12, 2026
1603c9c
Merge pull request #448 from ako/claude/mxcli-unit-test-perf-n7ggx8
ako Sep 12, 2026
014da96
Merge remote-tracking branch 'origin/main' into claude/mxcli-findings…
claude Sep 12, 2026
48f4cb2
fix(microflow): give an end event two paths reach a merge to join the…
claude Sep 12, 2026
f1af1a5
docs(proposal): measure MDL-FLOW01 prevalence, which selects Mode 3
claude Sep 12, 2026
b2a0c84
Merge pull request #449 from ako/claude/mxcli-findings-nnl181
ako Sep 12, 2026
6842085
Merge pull request #450 from ako/claude/mxcli-unit-test-perf-n7ggx8
ako Sep 12, 2026
c79e0b4
Merge pull request #451 from ako/claude/mxcli-issues-ovfoxk
ako Sep 12, 2026
3636722
fix(view-entity,widgets): read Mendix's own OQL clause order, and sto…
claude Sep 12, 2026
0c0bebc
feat(widget-describe): name the widgets-typed slots inside an object-…
claude Sep 12, 2026
6c12702
feat(view-entity): give a view entity its association to a persistent…
claude Sep 12, 2026
c6d0e25
fix(widgets): renumber the unroutable-child rule to MDL-WIDGET30, and…
claude Sep 12, 2026
5071aa4
Merge pull request #452 from ako/claude/mxbuild-diagnostics-spike-emta6h
ako Sep 12, 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
7 changes: 6 additions & 1 deletion .claude/skills/fix-issue/findings/mdl-backend.jsonl

Large diffs are not rendered by default.

13 changes: 12 additions & 1 deletion .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/sdk.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,4 @@
{"area": "sdk/mpr", "date": "2026-08-31", "symptom": "A document mxcli writes carries a different **typed-array marker** (the leading `int32` of a Mendix array) than the equivalent Studio Pro document — e.g. every list in a `CREATE OR REPLACE NAVIGATION` profile was `1` where Studio Pro writes `2` or `3`. No error, no warning, no build failure: it renders and opens", "cause": "The writers hand-build `bson.A{int32(1)}` per list. The marker is a **per-field constant**, not a function of the list's contents (`Forms$FormSettings.ParameterMappings` is `2` in 816 empty and 306 non-empty documents alike), so it cannot be derived — it has to be read off real documents", "file": "`sdk/mpr/writer_navigation.go` + `mdl/backend/modelsdk/navigation_write.go` + `modelsdk/mpr/nav_patch.go` (`navMarker*` / `navpMarker*` constants), `mdl/backend/modelsdk/navigation_profile_add.go`, `modelsdk/codec/defaults.go` (`RegisterListMarker`) for the codec paths", "insight": "**Census, don't reason.** Walk every `.mxunit` on the machine, tabulate `(parent $Type, field, marker, empty?)`, and take the value the Studio Pro documents carry — 19,078 files across 54 projects settled five of six navigation fields outright. **`int32(1)` is NOT invalid**, whatever `debug-bson.md` used to say: a Marketplace `.mpk` mxcli has never touched uses it for `CustomWidgets$WidgetValueType.AllowedTypes` (212k occurrences) and `Forms$Page.AllowedModuleRoles`. Believing otherwise turns a per-field mismatch into a phantom corruption bug and sends the fix in the wrong direction. Where the census has no observation, **find a document that has one** rather than picking: `HomeItems` was `2` in all 51 stored profiles but every one was empty, and `navigation_profile_add.go` wrote `3` from a PED session that could not be re-run. ako/TestApp settled it — a Studio Pro-authored profile whose `HomeItems` holds two `Navigation$RoleBasedHomePage` elements at marker **2**, the non-empty case the census could not reach. One project with the feature actually configured beats any amount of reasoning about empty lists. Verify by dumping the written document and the project's own pristine reference and diffing the marker column, not by `mx check`, which is silent on all of it"}
{"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": []}
10 changes: 10 additions & 0 deletions .claude/skills/mendix/alter-page/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,16 @@ dataview's entity as their context. Supported on simple containers (container,
dataview, groupbox, scroll-container region); for a layout grid or tab container,
insert relative to a widget inside the target column/tab instead.

**The context comes from the nearest enclosing data source, whatever kind it is**
— a database or association source, a microflow/nanoflow source (the entity is
the flow's return type), or `datasource: selection <list>`, which takes the
entity of the list it listens to. A bare attribute in the inserted or replaced
widget resolves against that entity, exactly as it would in `create page`. When
no enclosing source can be resolved, the binding is written unset rather than
guessed at — `describe page` then prints `<unbound>`, and mxbuild reports
`CE0402 "No value specified."`, so re-describe the page after an ALTER that
moves data-bound widgets.

### DROP - Remove Widgets

```sql
Expand Down
11 changes: 11 additions & 0 deletions .claude/skills/mendix/create-page/reference/widgets.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ describe icon collection Atlas_Core.Atlas_Filled -- every icon + its reference
```

**Action Bindings:**
- `action: nothing` - Deliberately no action (a decorative button, a card that is not clickable)
- `action: save_changes` - Save changes to object
- `action: save_changes close_page` - Save and close page
- `action: cancel_changes` - Cancel changes
Expand All @@ -128,6 +129,16 @@ describe icon collection Atlas_Core.Atlas_Filled -- every icon + its reference
variable is refused as **MDL-PAGEARG01** — it used to be accepted and silently
opened the page with the context object anyway. To open a page with something
else, call a microflow that shows it.
- **The list above is the whole vocabulary, and a keyword without its argument is
not in it.** `action: open_link` with no URL, `action: show_page` with no page,
`action: microflow` with no name — each is **MDL-WIDGET28**. Until
mendixlabs/mxcli#1062 these were written as a widget with *no action at all*:
it rendered, carried its caption, and did nothing, while `mxcli check`, `exec`
and mxbuild all reported success, because a no-action widget is legal Mendix.
An invented keyword (`action: totally_made_up`) did the same. Use
`action: nothing` when a control really is meant to be inert, so a dead one
always means a mistake.
- The same forms serve `onclick:` (an alias of `action:`) and `onchange:`.

**Button Styles:** `default`, `primary`, `success`, `info`, `warning`, `danger`, `inverse`
- Case-insensitive (`primary` and `Primary` both work).
Expand Down
20 changes: 19 additions & 1 deletion .claude/skills/mendix/custom-widgets/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,32 @@ The error now names the container keyword and rewrites your entry into the form
that works.

`describe widget <name> -p <project.mpr>` lists a widget's container keywords
under **Body containers**.
under **Body containers**, and — for an object list — the widgets-typed **slots
inside one item**, with the widget types that route into each:

```
column object list -> columns authorable
items: showContentAs, attribute, dynamicText, …
slot content -> content: any other widget in the item body
slot filter -> filter: textfilter | numberfilter | datefilter | dropdownfilter
```

Read that last line before guessing where something goes. It says a Data Grid 2
column filter is written directly in the **column's** braces — not in
`controlbar`, which is the grid-wide filter bar and renders "Unable to get
filter store" if you put a column filter there.

### When the name is not found

A name resolving to no installed definition is an **error** (MDL-WIDGET25, with
near-miss suggestions), and a container the parent does not declare is
MDL-WIDGET26. Both need `-p`: without a project, mxcli knows only its embedded
widgets, so it stays quiet rather than reporting every real widget as unknown.

MDL-WIDGET29 needs no project: `statictext` writes `Forms$Text`, a type Mendix
does not have, and the project that comes out cannot be *loaded* at all (`mx
check` and Studio Pro both stop at `TypeCacheUnknownTypeException` before
validation). Use `dynamictext` with a literal `Content:`.
If a widget you have installed is not found, extract its definition:

```bash
Expand Down
22 changes: 22 additions & 0 deletions .claude/skills/mendix/overview-pages/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,28 @@ column colStatus (attribute: "Status") { dropdownfilter f4 } -- Enumeration
-- Boolean columns: omit the filter entirely
```

**The filter goes inside the column's own braces.** A `filter { … }` block is
the GALLERY spelling of a different thing — the widget-wide filter bar, which a
data grid calls `controlbar`:

```sql
-- ✅ data grid: per-column filter, inside the column
datagrid dg (...) { column colName (attribute: Name) { textfilter f1 } }

-- ✅ gallery: the widget-wide filter bar, which the gallery calls `filter`
gallery g (...) { filter f { textfilter f1 } }

-- ❌ the gallery form on a data grid — MDL-WIDGET30
datagrid dg (...) { column colName (attribute: Name) filter f { textfilter f1 } }
```

That last line is worth reading twice: it is not a column with a filter block.
A widget is `type name (props) { body }`, so with the `filter` *outside* the
column's braces it parses as a column with **no body** followed by a separate
`filter` widget — which the grid has nowhere to put. It used to be dropped on
write with no diagnostic, so `DESCRIBE PAGE` showing a filterless column was the
only symptom; it is now refused at check and exec time.

## NewEdit Page Template

Form for creating or editing a single entity. **Requires a page parameter** to receive the object.
Expand Down
5 changes: 5 additions & 0 deletions .claude/skills/mendix/write-microflows/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ begin
end;
```

The sibling document annotation is **`@applyentityaccess`** — runs the flow under the current user's entity access rules rather than with full access, with the same absent-preserves rule and an explicit `(false)` to turn it off ([pitfalls](reference/pitfalls.md#apply-entity-access)).

Two rules follow, and both are enforced rather than documented-and-hoped:

- **An absent `@excluded` never un-excludes.** It means "the script does not say",
Expand Down Expand Up @@ -666,6 +668,9 @@ $Name = $Other/Name on error { return 'lookup failed'; };
change $Order (Status = Shipped) on error { log error 'could not ship'; return; };
log info node 'App' 'starting' on error { return; };
show message 'saved' on error { return; };

-- BLOCKING halts the client until dismissed; after `objects`, before `on error`.
show message 'Hello {1}' type Warning objects [$Name] blocking;
validation feedback $Order/Total message 'must be positive' on error { return; };
show page Module.Page on error { return; };
close page on error { return; };
Expand Down
54 changes: 52 additions & 2 deletions .claude/skills/mendix/write-microflows/reference/integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,71 @@ round-trip without dropping SOAP actions.
-- Structured form. Resolved SOAP references use normal qualified names.
$Root = call web service SampleSOAP.OrderService
operation FetchSampleItems
send mapping SampleSOAP.OrderRequest
send mapping SampleSOAP.OrderRequest from $Request
receive mapping SampleSOAP.OrderResponse
timeout 30
on error rollback;

-- Quoted raw IDs are accepted when old project references are dangling or unavailable.
$Root = call web service 'sample-service-id'
operation FetchSampleItems
send mapping 'sample-send-mapping-id'
receive mapping 'sample-receive-mapping-id';

-- Raw escape hatch emitted for unsupported SOAP fields.
$Root = call web service raw 'AQID';
```

### The request body: arguments OR a send mapping, never both

A call stores **one** request body (`Microflows$RequestBodyHandling`), so the two
forms are alternatives. Asking for both is refused as **MDL-SOAP01** by
`mxcli check` and by `exec` — the same function runs in each.

**Arguments** bind the operation's parameters, in the same `(Name = value)` form
every other call statement uses:

```mdl
$Order = call web service Clients.OrderSoapClient
operation GetOrder (OrderId = $Customer/OrderId)
receive mapping Clients.SoapOrdersImportMapping;
```

Mendix stores each one under a `ParameterPath`
(`http%3A//www.example.com/:GetOrder|OrderId`) built from the operation's request
body element. mxcli reads that element off the consumed service document and
builds the path, so the script names only the parameter. Two consequences:

- **The consumed service must be present and declare the operation.** An
operation mxcli cannot resolve is refused rather than written with a made-up
path — a wrong path reproduces the same error with different text in it.
- **A misspelled parameter name cannot be caught by `mxcli check`.** The names
live in the WSDL's inline schema, which mxcli does not parse; the error arrives
from mxbuild as **CE0178** "Body parameter mapping needs to be refreshed" —
which is also what you get if you omit arguments an operation requires.

**A send mapping** builds the whole body from an export mapping, and needs the
variable it maps **from**:

```mdl
call web service Clients.OrderSoapClient
operation SaveOrder
send mapping Clients.SoapOrderExportMapping from $NewSaveOrder;
```

`from $var` is not optional. An export mapping maps an object and Mendix stores
which one; without it the call builds as **CE0369** "Cannot use simple request
body, as the operation's body is complex".

### Why DESCRIBE sometimes still shows base64

`describe microflow` renders a SOAP call structurally only when re-executing that
MDL would reproduce the stored document exactly. A call configured beyond what
MDL spells — HTTP authentication, a custom location, SOAP headers, a per-parameter
export mapping, or a result typed from the WSDL rather than from an import
mapping — keeps the `call web service raw '<base64>'` form, which round-trips
byte for byte. That is deliberate: rendering it structurally would silently
normalise the call on the next `exec`.

**Design note:** the raw payload is base64-encoded BSON for the complete action
and is authoritative on re-exec. Treat this as round-trip support, not a
recommended authoring format for new integrations.
Expand Down
31 changes: 31 additions & 0 deletions .claude/skills/mendix/write-microflows/reference/pitfalls.md
Original file line number Diff line number Diff line change
Expand Up @@ -507,3 +507,34 @@ end;
| CE0008 | No action defined | Define action for activity |
| CW0094 | Variable never used | Remove unused variables or use them |
| MDL | Variable not declared | Use `declare $var type = value;` before SET |

## Apply entity access

`@applyentityaccess` before a `create microflow` (or `create rule`) sets Studio
Pro's **"Apply entity access"** checkbox: the flow runs under the **current
user's** entity access rules instead of with full access.

```mdl
@applyentityaccess
create microflow MyModule.ReadOwnOrders ()
returns list of MyModule.Order
begin
retrieve $Orders from MyModule.Order;
return $Orders;
end;
```

It is a **security** setting and it only ever narrows, so the rules mirror
`@excluded`:

- **An absent `@applyentityaccess` never turns it off.** It means "the script does
not say", so a `create or modify` that omits it preserves whatever is stored.
Before this was carried, every rewrite cleared the flag — *widening* what the
microflow could read and write, with `mxcli check`, mxbuild and the model all
perfectly happy. Measured across 342 microflows in 4 projects: every microflow
storing the flag came back without it, and nothing anywhere reported it.
- **Turning it off is explicit**: `@applyentityaccess(false)`.
- **Not available on a nanoflow.** A nanoflow runs in the client and Mendix stores
no such property. Writing it there is **MDL059**, not a silent no-op —
the same rule that catches `@applyentityacces` and any other annotation the
document does not read. The message names what that document does accept.
Loading
Loading