Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c07a7ff
fix(alter-page): one alias table, schema-shaped column writes, named …
claude Aug 18, 2026
77c40b9
Merge pull request #178 from ako/claude/sudoku-test-issue-46-iyxn30
ako Aug 18, 2026
6833c37
Merge branch 'mendixlabs:main' into main
ako Aug 18, 2026
54c8f95
fix(check,marketplace): resolve refs with -p, two new rules, quieter …
claude Aug 18, 2026
48db7dd
fix(domainmodel): write and read the calculated-attribute binding
claude Aug 18, 2026
3e70c51
chore: gofmt three files so `make lint` stops dirtying the tree
claude Aug 18, 2026
e1dcac2
feat(exprcheck): back the CatalogReader seam with the catalog
claude Aug 18, 2026
763a6a9
style: gofmt three files left unaligned by earlier commits
claude Aug 18, 2026
20a1f6b
fix(examples): drop a CE0111 from the #312 excluded-microflow example
claude Aug 18, 2026
2f14788
feat(check): MDL061/062/063 for three build errors check used to pass…
claude Aug 18, 2026
de9b6c3
fix(check): MDL-SEC20 warns unless the script enables security
claude Aug 18, 2026
b53a84b
feat(check): run expression type checking under --references
claude Aug 18, 2026
4f65a95
Merge pull request #179 from ako/claude/sudoku-test-issue-46-iyxn30
ako Aug 18, 2026
bcab2d8
Merge pull request #180 from ako/claude/bootstrap-prompt-smaller-37u3fu
ako Aug 18, 2026
e76779d
fix: flag a builtin property a widget cannot route; correct two shipp…
claude Aug 18, 2026
4856e58
Merge branch 'main' into claude/mxcli-issues-ovfoxk
ako Aug 18, 2026
8fd0085
Merge pull request #181 from ako/claude/mxcli-issues-ovfoxk
ako Aug 18, 2026
16ffb5d
Merge origin/main into the expression type-checking branch
claude Aug 18, 2026
36b1c53
fix(mappings): DESCRIBE reproduces the script that made the mapping
claude Aug 18, 2026
4ecbbce
Merge pull request #182 from ako/claude/exprcheck-catalog-reader
ako Aug 18, 2026
7479a2e
Merge pull request #183 from ako/claude/sudoku-test-issue-46-iyxn30
ako Aug 18, 2026
1225510
feat(exprcheck): type attribute paths, and catch the enum comparison
claude Aug 18, 2026
a629b52
Merge pull request #185 from ako/claude/exprcheck-attribute-paths
ako Aug 18, 2026
0fb99fb
Merge branch 'main' into claude/bootstrap-prompt-smaller-37u3fu
ako Aug 18, 2026
d3cdf48
Merge pull request #184 from ako/claude/bootstrap-prompt-smaller-37u3fu
ako Aug 18, 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
15 changes: 15 additions & 0 deletions .claude/skills/fix-issue.md

Large diffs are not rendered by default.

57 changes: 57 additions & 0 deletions .claude/skills/mendix/alter-page.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,63 @@ alter page MyModule.Customer_Edit {
};
```

## DataGrid 2 columns: how to address them, and what you can set

**Mendix stores no column name.** A DataGrid 2 column's schema has no name or
identifier key — the only human-facing label is its caption — so the name you
write in MDL is dropped:

```mdl
create or replace page Mod.P (...) {
datagrid dg1 (datasource: database Mod.Item) {
column colLabel (attribute: Label, caption: 'The Label') -- "colLabel" is not stored
}
};
```

`describe page` shows that column as `Label`, and that is the name `ALTER PAGE`
answers to:

```mdl
alter page Mod.P { SET Caption = 'Renamed' ON dg1.colLabel } -- WRONG: column not found
alter page Mod.P { SET Caption = 'Renamed' ON dg1.Label } -- correct
```

The derived name is, in order: **the bound attribute's short name**, else the
**sanitized caption**, else **`colN`** by position. `mxcli check` reports
**MDL-WIDGET16** when the name you wrote differs from the one that will address
the column, so you find out at authoring time rather than from a failed ALTER.

Two columns that derive the same name are ambiguous and ALTER refuses rather than
picking one — give them distinct captions.

### Setting column properties

Property names resolve against the keys the installed widget declares, so both
the schema key and mxcli's MDL alias work (`DynamicCellClass` and `ColumnClass`
both reach `columnClass`). An unknown name lists what *is* settable on that grid.

**Expression-valued properties take a Mendix expression, not a literal.**
`DynamicCellClass` and `Visible` are expressions, so a literal CSS class has to be
a quoted string *inside* the expression — doubled quotes in MDL:

```mdl
-- WRONG: the expression becomes a bare identifier, mxbuild reports CE0117
alter page Mod.P { SET DynamicCellClass = 'highlight' ON dg1.Label }

-- correct: the expression is the string literal 'highlight'
alter page Mod.P { SET DynamicCellClass = '''highlight''' ON dg1.Label }
```

This applies equally to `create page`; the two paths behave identically. A bare
identifier is not a valid Mendix expression, and mxbuild reports CE0117 against
the column.

Properties holding a **structured** value — `attribute`, `filter`, `content`,
actions — cannot be set by ALTER at all. It refuses them and points at
`create or replace page`, rather than writing a string where Mendix expects a
reference.

## Common Mistakes

| Mistake | Fix |
Expand Down
45 changes: 45 additions & 0 deletions .claude/skills/mendix/check-syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,24 @@ It also does not mean the script is *correct*. `mxcli check` validates MDL synta
and mxcli's own rules; it does not validate the Mendix model. Run
`mx check` (or `mxcli docker check -p app.mpr`) after applying a slice.

### `-p` resolves references — there is no separate opt-in

`mxcli check script.mdl` alone checks syntax and the semantic rules that need no
model. **Pass `-p` and it also resolves every reference** — modules, entities,
pages, microflows and icons — against that project:

```bash
mxcli check script.mdl # syntax + model-free rules
mxcli check script.mdl -p app.mpr # ... and every reference resolved
```

`--references` is implied by `-p` and is kept only so existing scripts keep
working. It used to be required, which meant `mxcli check script.mdl -p app.mpr`
printed an unqualified `Check passed!` having resolved nothing — a misspelled
icon or entity sailed through a command that had been handed the project. A run
without a project now says what it did not check, so a pass is never read as
more than it is.

## Pre-Flight Validation Checklist

Before writing any MDL, verify these requirements:
Expand Down Expand Up @@ -137,6 +155,33 @@ Run `mxcli syntax keywords` for the full list of 320+ reserved keywords.
| `page not found` | Page doesn't exist | Check qualified name with `--references` |
| `entity not found` | Typo or wrong module | Use fully qualified name |

## Two rules that only real validation used to catch

Both are decidable from the MDL alone and now fail `check`, because a project
found them the hard way — four scripts passed `check` with 0 errors, executed
cleanly, and `mx check` then reported them:

| Rule | MxBuild | What it catches |
|---|---|---|
| `MDL-SEC20` | CE0156 | `CREATE USER ROLE` with no **System** module role — nobody holding it can sign in or read System entities. Add `System.User`. **Warning by default, error when the script enables security** (see below). |
| `MDL-PAGE20` | CE5601 | A page with **parameters and a `Url`** where the URL has no segment for a parameter. Mendix binds each parameter from the URL, so the page cannot be opened by link. |

`MDL-SEC20`'s severity follows the security level, because the underlying error
does. Measured on Mendix 11.13: the same role is **CE0156 at security level
Prototype and no error at all at level Off**, where roles are stored but not
validated. A blank project ships `Off`. So the rule warns by default and is an
error only when the script itself contains `ALTER PROJECT SECURITY LEVEL` set to
something other than `Off` — at which point the author has said which world they
are in.

`MDL-PAGE20` accepts an attribute path in the segment (`url: 'p006/{Customer/Name}'`),
which is the usual shape — it matches the segment's leading name, not the whole
segment.

**`check` is still necessary, not sufficient.** Run `mx check` (or
`mxcli docker check`) after every `exec`; these two rules narrow the gap, they do
not close it.

## Validation Workflow

### Before Writing MDL
Expand Down
22 changes: 22 additions & 0 deletions .claude/skills/mendix/generate-domain-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,28 @@ create persistent entity Module.OrderLine (
- `calculated Module.Microflow` — also valid (`by` keyword is optional)
- `calculated` — bare form, marks as calculated but requires manual microflow binding in Studio Pro

**The microflow's signature is checked, and mxcli refuses a mismatch before
writing** — Mendix reports these as **CE7247** at build time (verified on 11.13.0):

| Microflow | Result |
|-----------|--------|
| takes the owning entity (`$Order: Module.Order`) | ✅ stored with `PassEntity = true` |
| takes **no** parameter | ✅ stored with `PassEntity = false` — equally valid |
| takes a *different* entity | ❌ refused: CE7247 *"Microflow parameter 'X' should be of type Module.Order."* |
| takes two or more parameters | ❌ refused |
| returns the wrong type | ❌ refused: CE7247 *"Microflow return type should be …"* |
| returns `Long` for an `integer` attribute (or vice versa) | ✅ accepted — Integer and Long are one family here |

A microflow **created earlier in the same script** cannot be inspected yet, so
its signature is not checked; the build has the last word on those.

> **Before mxcli 0.17 the binding was silently discarded** on the default
> engine: the attribute was written as an ordinary stored value, `mx check`
> reported 0 errors, and the attribute stayed empty at runtime (#917). If you
> have attributes that were declared `calculated by` and never calculated, they
> need re-running through a current mxcli — re-executing the same statement is
> enough.

### Data Types

| Type | Example | Description |
Expand Down
11 changes: 8 additions & 3 deletions .claude/skills/mendix/json-structures-and-mappings.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,14 @@ Consequences worth knowing:

- **Either spelling works in MDL.** `Total = total` and `Total = Total` produce the
same stored mapping. Write whichever you have.
- **`DESCRIBE` emits the exposed name**, because that is the name Studio Pro shows.
A describe → edit → exec cycle is therefore lossless, but the text you get back
will not match the raw JSON keys you wrote.
- **`DESCRIBE` emits the raw JSON key**, so its output reproduces the script that
produced the mapping — `Total = total` comes back as `Total = total`, and an
array binding as `= item` rather than `= ItemItem`. It also emits
`create or modify`, so the output re-runs against the project it was read from.
(Until #915 it printed the exposed name and a bare `create`: the text differed
from the input, making every script-vs-describe diff noise, and re-running it
failed with "import mapping already exists". The stored mapping was correct
either way.)
- **A member matching neither spelling is refused**, listing what would have
worked. It is never written with a guessed path: such a mapping passed
`mxcli check` and failed later in mxbuild (CE5015) or at runtime.
Expand Down
37 changes: 37 additions & 0 deletions .claude/skills/mendix/migrate-design-prototype.md
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,43 @@ for screenshotting the running app. Iterate ②–④ per screen until it matche

---

## Never put a grid on a Mendix widget's own class

A layout that should be two columns comes out as one, and the CSS is right — it
is on the wrong element. Mendix wraps a repeating widget's children in an
intermediate element, so `display: grid` on the widget's own class has exactly
**one** grid item and every card stacks:

```
div.mx-listview.my-cards [689x2054] display=grid <- the class you wrote
ul. [334x2038] display=block <- ONE child
li.mx-name-index-0 [334x526] <- the things you meant to lay out
```

A data view does the same with `.mx-dataview-content`. One project hit this
twice in two different widgets before naming the rule (mxcli-owid, findings #15
and #41).

Put the grid on the element that actually holds the repeated children:

```scss
/* list view */
.my-cards > ul { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
.my-cards > ul > li { min-width: 0; }

/* data view */
.my-page > .mx-dataview-content { display: grid; grid-template-columns: 240px 1fr; }
```

`min-width: 0` on the child matters: a grid item defaults to `min-width: auto`,
so a wide table or a long unbroken string inside a card pushes the column past
its track instead of scrolling within it.

**How to find the right element** rather than guess: run the app, inspect the
widget, and walk down from the class you wrote until you reach the element with
one child per row. `mxcli run --local --screenshot` plus the browser inspector
settles it in one pass — see `.claude/skills/verify-in-runtime.md`.

## Gotchas (learned building this app)

- **Never put `Style:` (inline style) on a `DYNAMICTEXT`** — it crashes MxBuild with a
Expand Down
62 changes: 62 additions & 0 deletions .claude/skills/mendix/write-microflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,9 @@ declare $ProductList list of Test.Product = empty; -- use a parameter, retrieve
-- WRONG: Using AS keyword (not supported in mxcli)
declare $Product as Test.Product; -- ERROR: parse error

-- WRONG: No value (CE0038, MDL061)
declare $X string; -- a Create Variable activity requires a value

-- WRONG: Missing type
declare $Counter = 0; -- Type inference not always supported

Expand Down Expand Up @@ -390,6 +393,65 @@ end;

**Note**: Parameters are automatically declared by the parameter list. The `returns type as $Var` syntax names the return variable but does NOT declare it - you must still use `declare $Var type = value;` if you want to use SET on it.

### 8. RETURN Inside a Loop

**Error**: CE0068 - "End events cannot be placed inside a loop." (MDL062)

A `return` builds an End event, and Mendix does not allow one inside a loop —
whether the return sits in the loop body directly or inside a branch within it.

❌ **INCORRECT:**
```mdl
loop $Part in $PartList
begin
if $Part/IsMatch then
return true; -- End event inside the loop
end if;
end loop;
```

✅ **CORRECT** — leave the loop with `break`, and return once after it:
```mdl
declare $Found boolean = false;
loop $Part in $PartList
begin
if $Part/IsMatch then
set $Found = true;
break;
end if;
end loop;
return $Found;
```

### 9. Two Activities Creating the Same Variable

**Error**: CE0111 - "Duplicate variable name 'X'." (MDL063)

A microflow's variable names are unique **flow-wide**. Branches and loop bodies
do not open a scope, and parameters and loop iterators share the same namespace.
The trap is that every activity with an output **creates** its variable — there
is no form in which a call, a retrieve, an aggregate or an import mapping writes
into one that already exists.

❌ **INCORRECT:**
```mdl
declare $Session string = '';
$Session = call microflow Mod.Login(); -- the call creates $Session too
```

✅ **CORRECT** — let the activity create it:
```mdl
$Session = call microflow Mod.Login();
```

Assigning to an existing variable is fine, because `set` is a *Change Variable*
activity and creates nothing:

```mdl
declare $Session string = '';
set $Session = 'anonymous'; -- valid, any number of times
```

## Control Flow

### IF Statements
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/packs/mendix-odata-pushdown/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ builds the invocation for a resource backed by a stored routine.
| `Top`, `Skip` | bind | the page, already clamped to `MaxTop` |
| `SortColumn1/2`, `SortDirection1/2` | bind | the sort, as exposed names and `A`/`D` |
| `WantsCount` | both | `$count=true` — the client wants the size of the set |
| `Rejected`, `RejectReason` | both | the request asked for something untranslatable |
| `Rejected`, `RejectReason` | bind | the request asked for something untranslatable. Only a **bind** caller sees these: with `RejectUnsupported = true` (what a splice caller passes) `Parse` throws instead of returning, so a splice caller always has `Rejected = false` |

## Two ways to spend it

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,15 @@ time it is asked for anyway, so the requirement is doing you a favour.
**An untranslated filter, on a splice caller that passed `RejectUnsupported = false`.**

The splice caller's `WHERE` *is* `FilterSql`. If the filter could not be
translated and was dropped, there is no `WHERE`. Pass `true`: `Rejected` comes
back set, and the caller is expected to fail the request.
translated and was dropped, there is no `WHERE` — every row in the table, under
a 200, in answer to a request for a handful.

Pass `true`. `Parse` then **throws** rather than returning a Query, so the
request becomes a 500 with the reason in the runtime log. Do not write a
microflow branch on `Rejected` for this: a splice caller that receives a Query
at all always has `Rejected = false`, so the branch is dead code (mxcli-owid,
finding #30). Reading `Rejected` is for a **bind** caller, which passes `false`
and does get a Query back.

`$orderby` is the one thing dropped rather than rejected. A wrong order is
cosmetic; a wrong row count is not.
Expand Down
25 changes: 21 additions & 4 deletions .claude/skills/packs/mendix-odata-pushdown/references/patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,34 @@ $Q = CALL JAVA ACTION {{MODULE}}.Parse(
KeyField = 'driverId',
RejectUnsupported = true);

IF $Q/Rejected THEN
-- fail the request; do not answer it with unfiltered rows
END

DECLARE $Sql String = 'SELECT d.* FROM drivers d' + $Q/FilterSql + $Q/OrderBySql;
```

**Splice callers should pass `RejectUnsupported = true`.** Their `WHERE` *is*
`FilterSql`, so an untranslated filter means no `WHERE` at all — every row in
the table, under a 200, in answer to a request for a handful.

**Do not write `IF $Q/Rejected THEN` in a splice caller — the branch cannot
run.** With `RejectUnsupported = true`, `Parse` throws before it builds the
Query:

```java
if (r.rejected && Boolean.TRUE.equals(rejectUnsupported)) {
throw new IllegalArgumentException("cannot translate OData query: " + r.rejectReason);
}
IMendixObject o = Core.instantiate(context, ENTITY); // never reached when rejected
```

So a microflow that receives a `Query` at all always has `Rejected = false`. The
untranslatable request has already become a 500 with the reason in the runtime
log — which is the honest answer, and is why the throw is there. A guard
microflow written for that branch never executes; one project wrote and then
deleted a Java action for exactly this (mxcli-owid, finding #30).

`Rejected` / `RejectReason` are still worth reading — by a **bind** caller
(`RejectUnsupported = false`), which does receive a Query and needs to log the
filter it was never going to apply.

---

## Bind — the SQL is somebody else's
Expand Down
Loading
Loading