Makepad Widgets
sickn33/agentic-awesome-skills
Version: makepad-widgets (dev branch) | Last Updated: 2026-01-19 Check for updates: https://crates.io/crates/makepad-widgets
MDL syntax for pluggable widgets in CREATE PAGE / ALTER PAGE — any installed widget is named by its own name (htmlelement frame (…) { … }), with object lists and child slots read from its definition.
$ npx skills add mendixlabs/mxcli --skill custom-widgets -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install mendixlabs/mxcli custom-widgets --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/mendixlabs/mxcli.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.claude/skills/mendix/custom-widgets .claude/skills/custom-widgets && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "custom-widgets" agent skill from https://github.com/mendixlabs/mxcli/tree/main/.claude/skills/mendix/custom-widgets into .claude/skills/custom-widgets/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "custom-widgets", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/mendixlabs/mxcli/tree/main/.claude/skills/mendix/custom-widgetsType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add mendixlabs/mxcli --skill custom-widgets -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install mendixlabs/mxcli custom-widgets --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/mendixlabs/mxcli.git skills-src && mkdir -p .agents/skills && cp -r skills-src/.claude/skills/mendix/custom-widgets .agents/skills/custom-widgets && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "custom-widgets" agent skill from https://github.com/mendixlabs/mxcli/tree/main/.claude/skills/mendix/custom-widgets into .agents/skills/custom-widgets/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "custom-widgets", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add mendixlabs/mxcli --skill custom-widgets -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install mendixlabs/mxcli custom-widgets --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/mendixlabs/mxcli.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/.claude/skills/mendix/custom-widgets .cursor/skills/custom-widgets && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "custom-widgets" agent skill from https://github.com/mendixlabs/mxcli/tree/main/.claude/skills/mendix/custom-widgets into .cursor/skills/custom-widgets/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "custom-widgets", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/mendixlabs/mxcli.git --path .claude/skills/mendix/custom-widgets--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add mendixlabs/mxcli --skill custom-widgets -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install mendixlabs/mxcli custom-widgets --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/mendixlabs/mxcli.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/.claude/skills/mendix/custom-widgets .gemini/skills/custom-widgets && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "custom-widgets" agent skill from https://github.com/mendixlabs/mxcli/tree/main/.claude/skills/mendix/custom-widgets into .gemini/skills/custom-widgets/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "custom-widgets", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install mendixlabs/mxcli custom-widgetsInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add mendixlabs/mxcli --skill custom-widgets -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/mendixlabs/mxcli.git skills-src && mkdir -p .github/skills && cp -r skills-src/.claude/skills/mendix/custom-widgets .github/skills/custom-widgets && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "custom-widgets" agent skill from https://github.com/mendixlabs/mxcli/tree/main/.claude/skills/mendix/custom-widgets into .github/skills/custom-widgets/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "custom-widgets", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add mendixlabs/mxcli --skill custom-widgets -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install mendixlabs/mxcli custom-widgets --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/mendixlabs/mxcli.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/.claude/skills/mendix/custom-widgets .opencode/skills/custom-widgets && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "custom-widgets" agent skill from https://github.com/mendixlabs/mxcli/tree/main/.claude/skills/mendix/custom-widgets into .opencode/skills/custom-widgets/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "custom-widgets", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
custom-widgetsMDL syntax for pluggable widgets in CREATE PAGE / ALTER PAGE — any installed widget is named by its own name (htmlelement frame (…) { … }), with object lists and child slots read from its definition.
Custom Widgets is an agent skill from mendixlabs/mxcli. MDL syntax for pluggable widgets in CREATE PAGE / ALTER PAGE — any installed widget is named by its own name (htmlelement frame (…) { … }), with object lists and child slots read from its definition. Covers GALLERY, COMBOBOX, DataGrid2, charts and third-party widgets: datasource and column forms, child slots (TEMPLATE/FILTER), the pluggablewidget '<id' fallback, and adding a widget via .def.json. Use when placing a pluggable widget on a page, or when mxcli widget describe output needs interpreting. For the…
Its SKILL.md is about 7.8k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.
The repository describes itself as: Mendix cli tool, a headless way to work with Mendix projects. Enables Mendix projects for use with 3rd party agentic coding tools like Claude Code and Copilot. Includes a… The licence is Apache-2.0.
4 steps, taken from the step headings in SKILL.md.
Read from SKILL.md and the folder at commit a924d11. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
No scripts in the folder and no shell commands in SKILL.md (its code samples are sql, bash and json).
From the folder's file list and the shell code blocks in SKILL.md.
No URLs in SKILL.md.
From URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Custom Widgets loads about 7.8k tokens when it runs. Until then it costs about 149 tokens; SKILL.md has 2,835 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from mendixlabs/mxcli at commit a924d11, republished under its Apache-2.0 licence (© mendixlabs). 2,835 words, ~7,765 tokens.
.claude/skills/custom-widgets/SKILL.md (or your agent's skills folder).If a widget is installed in widgets/, MDL names it directly — no keyword list,
no widget id:
htmlelement frame (tagName: 'div', tagContentMode: 'container') {
attribute a1 (attributeName: 'data-testid', attributeValueType: 'expression')
tagcontentcontainer body {
dynamictext caption (Content: 'Inside the element')
}
}Three things there are read from the widget's definition, not from anything
hardcoded: the keyword (htmlelement, the last segment of the widget id),
the properties (the widget's own spelling — tagName, not TagName), and
the body containers — attribute is an object list (one entry per
repetition), tagcontentcontainer a child slot (holds widgets).
Ask the widget rather than guessing. describe widget type <name> lists every
property with its type, default and enumeration members; every body container
and whether MDL can express it; and a complete example that parses AND checks as
written:
mxcli widget describe htmlelement -p app.mprDo this first when placing an unfamiliar widget. It is faster than reading this
file and it cannot go stale, because it reads the .mpk the project actually
has.
pluggablewidget 'com.mendix.widget.web.htmlelement.HTMLElement' frame (tagName: 'div')Use it only when two installed packages ship the same MDL name, or when you have the id and not the name. Everything below that still shows the id form works unchanged — the short form is simply the better default.
A widget's repeatable property — FileUploader allowedFileFormats, HTML Element
attributes, a chart's series — is written as container blocks in the body:
htmlelement frame ( tagName: 'div' ) {
attribute a1 (attributeName: 'data-testid', attributeValueType: 'expression')
}Not as a property value:
htmlelement frame ( attributes: [(attributeName: 'data-testid')] ) -- MDL-WIDGET27That form is an error (mendixlabs/mxcli#999). It used to be worse than an
error: the single-key shape checked clean, exec'd successfully and the property
vanished from storage, while the multi-key shape died as missing ')' at ','.
The error now names the container keyword and rewrites your entry into the form
that works.
The same rule covers the two spellings that carry no entry to key on
(mendixlabs/mxcli#1056):
selectionhelper sh (renderStyle: 'custom', customAllSelected: []) -- MDL-WIDGET27
selectionhelper sh (renderStyle: 'custom', customAllSelected: 'something') -- MDL-WIDGET27A widgets-typed property such as customAllSelected holds child widgets, so
it is written as a block with widgets in it rather than entries:
selectionhelper sh (renderStyle: 'custom') {
customallselected s1 { dynamictext d1 (Content: 'All') }
}The empty form is reported from its shape, with no project needed. The scalar
form is reported only when the widget resolves, because without a definition
p: 'x' is the ordinary property form and flagging it would be a guess. Both
matter because a required slot left empty is not a silent no-op at build time —
it is CE0642 "Property '…' is required.", one per slot.
describe widget type <name> -p <project.mpr> lists a widget's container keywords
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 | dropdownfilterRead 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.
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:
mxcli widget init -p app.mprCard-layout list with optional template content and filters.
gallery galleryName (
datasource: database from Module.Entity sort by Name asc,
selection: single | multiple | none,
DesktopColumns: 3,
TabletColumns: 2,
PhoneColumns: 1
) {
template {
dynamictext title (content: '{1}', contentparams: ({1} = Name), rendermode: H4)
dynamictext info (content: '{1}', contentparams: ({1} = Email))
}
filter {
textfilter searchName (attribute: Name)
numberfilter searchScore (attribute: Score)
dropdownfilter searchStatus (attribute: status)
datefilter searchDate (attribute: CreatedAt)
}
}template block -> mapped to content property (child widgets rendered per row)filter block -> mapped to filtersPlaceholder property (shown above list)selection: none omits the selection property (default if omitted)DesktopColumns, TabletColumns, PhoneColumns control responsive grid columns (default: 1 each, omit if default)mdlContainer: "template"Two modes depending on the attribute type:
-- Enumeration mode (Attribute is an enum)
combobox cbStatus (label: 'Status', attribute: status)
-- Association mode (Attribute is an association)
combobox cmbCustomer (
label: 'Customer',
attribute: Order_Customer,
datasource: database Module.Customer,
CaptionAttribute: Name
)datasource is present (hasDataSource condition)CaptionAttribute is the display attribute on the target entityA widget may expose several datasources. Address one by its own property key
(or a registered alias) instead of the generic datasource: clause:
combobox cmbCustomer (
Association: Order_Customer,
optionsSourceAssociationDataSource: database from Module.Customer,
CaptionAttribute: Name
)The value has to be a datasource, not an entity name. optionsSourceAssociationDataSource: Module.Customer
is MDL-WIDGET05: it names an entity, cannot be stored as a datasource, and
before mxcli rejected it, it passed check and exec and then failed the build
with CE0642 against a property nobody had mentioned (mendixlabs/mxcli#643).
A isLinked datasource is not yours to set. A widget.xml
isLinked="true" datasource is filled from the CONTAINING widget — a Data Grid 2
supplies its column filter's linkedDs ("Datasource to Filter"). A .def.json
mapping one is refused at build time. Measured on 11.6.6: five Studio
Pro-authored drop-down filters store it empty, a filter written without it passes
mx check at 0 errors, and a filter written WITH it still fails CE0642
"Property 'Datasource to Filter' is required" — mxbuild resolves the property
from the parent rather than reading what is stored, so writing it is not merely
useless. Across every widget package in testdata/expr-checker, linkedDs is
the only linked datasource among the eight multi-datasource widgets, which is why
DROPDOWNFILTER is single-source from MDL's side while COMBOBOX and the charts are
not.
The generic datasource: clause stays the convenience form for a
single-datasource widget. On one exposing several it names nothing in
particular and is refused, with the keys to use instead -- neither guess is
defensible: feeding it to every mapping duplicates one binding across unrelated
slots, and feeding it to the first leaves the others unset (CE0642 again).
An unqualified attribute binds where the widget says. A property widget.xml
links to a datasource (dataSource="parts") binds to its items; one linked to
none binds to the enclosing data container's object, not the widget's own data.
A name of another entity in scope is refused, naming the candidates (#647).
describe page emits the named keys back when a widget has several configured
sources, so describe -> exec keeps each binding on its own mapping. A widget with
ONE source keeps the generic DataSource: clause it has always been described
with. A source whose schema key cannot be resolved falls back to the generic
spelling rather than being dropped.
Charts are pluggable widgets. Install Charts.mpk into the project's widgets/
folder first (any Charts-based app has it); exec auto-generates the
.def.json.
Each is authorable by its own name — barchart, linechart, piechart,
heatmap — and the examples below use the package id form, which also still
works. The id column is kept because it is what describe widget prints and
what identifies the widget unambiguously.
Chart type → widget id → data container:
| Chart | Widget id (pluggablewidget '…') | Data block |
|---|---|---|
| Bar / Column / Area | com.mendix.widget.web.{barchart.BarChart, columnchart.ColumnChart, areachart.AreaChart} | series (one or more) |
| Line / TimeSeries / Bubble | com.mendix.widget.web.{linechart.LineChart, timeseries.TimeSeries, bubblechart.BubbleChart} | line (one or more) |
| HeatMap | com.mendix.widget.web.heatmap.HeatMap | widget-level attrs + scalecolor items |
| Pie | com.mendix.widget.web.piechart.PieChart | widget-level attrs (no object-list) |
Series / line — each binds its OWN datasource + X/Y:
pluggablewidget 'com.mendix.widget.web.barchart.BarChart' chart1 {
series s1 (
dataSet: 'static',
DataSource: database from MyModule.SalesByRegion, -- an OQL VIEW (aggregated)
staticXAttribute: Region, -- resolves against the series' own datasource
staticYAttribute: Total,
staticName: 'Revenue',
interpolation: 'linear' -- line/area only: linear | spline
)
}A series datasource takes any of the usual kinds — database from …,
microflow …, nanoflow …, $Param, selection … — not just database.
(Before #941 describe page rendered every series datasource as database from, so a microflow-backed series described back as a missing entity.)
Pie / HeatMap bind at the WIDGET level (no series block). Both need DataSource:
ValueAttribute:; Pie also needs a required SeriesName:; HeatMap adds scalecolor items:pluggablewidget 'com.mendix.widget.web.piechart.PieChart' pie1 (
DataSource: database from MyModule.SalesByRegion,
ValueAttribute: Total,
seriesName: 'Sales by Region' -- REQUIRED (CE4899 without it)
)
pluggablewidget 'com.mendix.widget.web.heatmap.HeatMap' heat1 (
DataSource: database from MyModule.SalesByRegion,
ValueAttribute: Total -- REQUIRED (CE0642 without it)
) {
scalecolor scLow (valuePercentage: 0, colorValue: '#f7fbff')
scalecolor scHigh (valuePercentage: 100, colorValue: '#08306b')
}Per-chart required-property gotchas (all are mxbuild errors, not check errors):
StaticXAttribute MUST be a Date and time attribute (CE7247 otherwise). Feed it a view with a datetime column.line needs a StaticSizeAttribute: (a numeric) in addition to X/Y.SeriesName: is required (CE4899); ValueAttribute: is required (CE0642).ValueAttribute: is required (CE0642).Data feed = OQL view entities. Charts want aggregated data (one row per
category). Build a create view entity … as select … group by … and point the
chart's DataSource: at it. Never name a view column after an OQL keyword
(Quarter/Month/Year/Day → CE0174); use Period etc. (check warns —
MDL032).
CE0463 "update this widget" is EXPECTED after generating charts. mxcli writes
the WidgetType from an embedded 11.6 baseline; the installed Charts.mpk is a
different version, so Studio Pro/mxbuild flags drift. Clear it with mxcli fix widgets (keeps your storage format); docker check only normalizes a temp copy,
so check the stored project with --no-update-widgets. The whole
mdl-examples/doctype-tests/34-chart-widget-examples.mdl builds 0 errors after.
Do NOT run bare mx update-widgets on an MPRv2 project (an mprcontents/-folder
project — what mxcli new creates): it converts the project to single-file v1 and
deletes mprcontents/, corrupting git, breaking a running mxcli run --local
loop, and sometimes making the project unopenable in Studio Pro. mxcli fix widgets
writes the result back as v2, mxcli docker check runs on a temporary copy; raw
mx update-widgets is only safe on a v1 project or a throwaway diagnostic copy.
DESCRIBE round-trips series/line/scalecolor object-lists (item names are
synthesized, e.g. series1); a Pie/HeatMap's widget-level SeriesName/datasource
are not yet reconstructed.
mxcli widget extract --mpk widgets/MyWidget.mpk
# Output: .mxcli/widgets/mywidget.def.json
# Override MDL keyword
mxcli widget extract --mpk widgets/MyWidget.mpk --mdl-name MYWIDGETThe extract command parses the .mpk (ZIP archive containing package.xml + widget XML) and auto-infers operations from XML property types:
| XML Type | Operation | MDL Source Key |
|---|---|---|
| attribute | attribute | attribute |
| association | association | association |
| datasource | datasource | datasource |
| selection | selection | selection |
| widgets | widgets (child slot) | container name (key uppercased) |
| boolean/string/enumeration/integer/decimal | primitive | hardcoded value from defaultValue |
| textTemplate | texttemplate | TextTemplate |
| action | action | OnClick / OnChange, else the property's own key |
| expression/object/icon/image/file | skipped | too complex for auto-mapping |
Skipped types require manual configuration in the .def.json.
Action slots are matched by name, and the storage key is not the MDL name.
Mendix's own widgets suffix theirs — a BadgeButton's click slot is onClickEvent,
a HeatMap's is onClickAction, a Combobox's change slot is onChangeEvent —
so actionSourceForKey strips one Event/Action suffix before matching
onclick/onchange. That is what lets onClick: and OnChange: reach those
widgets at all.
Every other action slot is authored by the widget's own key — a named slot:
FILEUPLOADER fu (
createFileAction: microflow MyModule.ACT_CreateFile,
onUploadSuccessFile: microflow MyModule.ACT_AfterUpload
)In the .def.json a named slot is a mapping with no source, the same shape
object-list item mappings use:
{"propertyKey": "createFileAction", "operation": "action"}microflow/nanoflow on a named slot parse as a data source — those forms
overlap with dataSourceExprV3 and the datasource alternative has to win, or a
chart series' staticDataSource: microflow M.X would become an action. The
executor converts them, because the widget definition is the only layer that
knows the slot is action-typed. Every other action form (show page,
save changes, …) reaches the AST as an action directly.
A slot may be conditional, and writing into a pruned one is CE0463. DataGrid 2's
onSelectionChange is hidden when itemSelection = None, so it needs
Selection: Multiple (or Single) alongside it. mxcli check refuses the
statement with MDL-WIDGET10 rather than letting the build fail. mxcli widget describe <name> lists each slot's hidden when condition.
Object-list item action slots (chart series staticOnClickAction, popupmenu
item action) have mappings generated but the engine still skips them at apply
time. See upstream #956.
The .def.json only describes mapping rules. The engine also needs a template JSON with the complete Type + Object BSON structure.
# 1. in Studio Pro: drag the widget onto a test page, save the project
# 2. Extract the widget's BSON:
mxcli bson dump -p App.mpr --type page --object "Module.TestPage" --format json
# 3. Extract the type and object fields from the customwidget, save as:Place at: project/.mxcli/widgets/mywidget.json
Template JSON format:
{
"widgetId": "com.vendor.widget.MyWidget",
"name": "My widget",
"version": "1.0.0",
"extractedFrom": "TestModule.TestPage",
"type": {
"$ID": "aa000000000000000000000000000001",
"$type": "CustomWidgets$CustomWidgetType",
"WidgetId": "com.vendor.widget.MyWidget",
"PropertyTypes": [
{
"$ID": "aa000000000000000000000000000010",
"$type": "CustomWidgets$WidgetPropertyType",
"PropertyKey": "datasource",
"ValueType": { "$ID": "...", "type": "datasource" }
}
]
},
"object": {
"$ID": "aa000000000000000000000000000100",
"$type": "CustomWidgets$WidgetObject",
"TypePointer": "aa000000000000000000000000000001",
"properties": [
2,
{
"$ID": "...",
"$type": "CustomWidgets$WidgetProperty",
"TypePointer": "aa000000000000000000000000000010",
"value": {
"$type": "CustomWidgets$WidgetValue",
"datasource": null,
"AttributeRef": null,
"PrimitiveValue": "",
"widgets": [2],
"selection": "none"
}
}
]
}
}CRITICAL: Template must include both type (PropertyTypes schema) and object (default WidgetObject with all property values). Extract from a real Studio Pro MPR -- do NOT generate programmatically. Mismatched structure causes CE0463.
project/.mxcli/widgets/mywidget.def.json <- project scope (highest priority)
project/.mxcli/widgets/mywidget.json <- template json (same directory)
~/.mxcli/widgets/mywidget.def.json <- global scopeSet "templateFile": "mywidget.json" in the .def.json. Project definitions override global ones; global overrides embedded.
MYWIDGET myWidget1 (datasource: database Module.Entity, attribute: Name) {
template content1 {
dynamictext label1 (content: '{1}', contentparams: ({1}=Name))
}
}When mxcli runs with --mcp (writes routed to a running Studio Pro), pluggable
widgets take a different, simpler path than the MPR writer:
.def.json
(Step 1) is required. Studio Pro owns serialization over pg_patch_page and
expands every default, so the CE0463 template-mismatch class does not exist
on this path..mxcli/widgets/ -> global -> embedded). There is no separate MCP
whitelist.{AttrName} placeholders and <Name>Params /
contentparams bindings -> template parameters),
and action (microflow Module.Flow, show page Module.Page, or none).imageUrl unless ImageType: 'imageUrl' is also set. If a property
you set does not appear in Studio Pro, check the widget's mode selector.{
"widgetId": "com.vendor.widget.web.mywidget.MyWidget",
"mdlName": "MYWIDGET",
"templateFile": "mywidget.json",
"defaultEditable": "Always",
"propertyMappings": [
{"propertyKey": "datasource", "source": "datasource", "operation": "datasource"},
{"propertyKey": "attribute", "source": "attribute", "operation": "attribute"},
{"propertyKey": "someFlag", "value": "true", "operation": "primitive"}
],
"childSlots": [
{"propertyKey": "content", "mdlContainer": "template", "operation": "widgets"}
],
"modes": [
{
"name": "association",
"condition": "hasDataSource",
"propertyMappings": [
{"propertyKey": "optionsSource", "value": "association", "operation": "primitive"},
{"propertyKey": "assocDS", "source": "datasource", "operation": "datasource"},
{"propertyKey": "assoc", "source": "association", "operation": "association"}
]
},
{
"name": "default",
"propertyMappings": [
{"propertyKey": "attr", "source": "attribute", "operation": "attribute"}
]
}
]
}| Condition | Checks |
|---|---|
hasDataSource | the generic datasource: clause is set, OR any of THIS mode's datasource mappings was given by name |
hasDataSource:KEY | the datasource property KEY was given (by its key or an alias) |
hasAttribute | AST widget has an attribute property |
hasProp:XYZ | AST widget has a property named XYZ |
Modes are evaluated in definition order -- first match wins. A mode with no condition is the default fallback.
Use hasDataSource:KEY when several modes are told apart by WHICH datasource is
set -- a ComboBox's association vs database mode. Bare hasDataSource cannot
distinguish them, so with two such modes the one listed first always wins.
Bare hasDataSource only consults the mode's own datasource mappings, never
every datasource-shaped property on the widget: a microflow action and a
microflow datasource parse to the same AST shape, so a widget's OnChange: would
otherwise select a datasource mode.
| Operation | What it does | Typical Source |
|---|---|---|
attribute | Sets Value.AttributeRef on a WidgetProperty | attribute |
association | Sets Value.AttributeRef + Value.EntityRef | association |
primitive | Sets Value.PrimitiveValue | static value or property name |
datasource | Sets Value.DataSource (serialized BSON) | datasource |
selection | Sets Value.Selection (mode string) | selection |
widgets | Replaces Value.Widgets array with child widget BSON | child slot |
texttemplate | Sets text in Value.TextTemplate (Forms$ClientTemplate) | property name (resolved as string) |
A texttemplate takes text, so a bare value renders the same string on every
row. Bind it with the property's own <Name>Params companion, named for
whichever spelling the template used (ImageUrl: pairs with ImageUrlParams:)
and taking the same format (...) block a dynamictext does — e.g.
headerCaption: '{1}', headerCaptionParams: ({1} = Name), or a Timeline's
title / description bound separately. contentparams: is ONE list shared by
every template on the widget, so it only disambiguates a widget with a single
one; '{AttrName}' is the short form for one attribute. A companion whose
template has no {N} is MDL-WIDGET21, not a silent drop (ako/mxcli#575).
| action | Sets Value.Action with serialized client action BSON | onclick (resolved from AST Action) |
association source must come AFTER datasource source in the mappings array. The association operation depends on entityContext set by a prior DataSource mapping. The registry validates this at load time.value takes priority over source: if both are set, the static value is used.Order is NOT how a dependent property finds its entity on a multi-datasource
widget. The widget's own package states that per property (widget.xml's
dataSource="..."), and mxcli reads it: a DropdownFilter's refCaption binds
against refOptions' entity and its attr against linkedDs', whatever order
the mappings are in. A property that declares no dataSource falls back to the
shared entity context, which is every property of every single-datasource
widget -- so the ordering rule above still describes what happens there.
| Source | Resolution logic |
|---|---|
attribute | w.GetAttribute() -> pageBuilder.resolveAttributePath() |
datasource | w.GetDataSource() -> pageBuilder.buildDataSourceV3() -> also updates entityContext |
association | w.GetAttribute() -> pageBuilder.resolveAssociationPath() + uses current entityContext |
selection | w.GetSelection() or mapping.Default fallback |
CaptionAttribute | w.GetStringProp("CaptionAttribute") -> auto-prefixed with entityContext if relative |
| (other) | Treated as generic property name: w.GetStringProp(source) |
When buildWidgetV3() encounters an unrecognized widget type:
1. Registry lookup: widgetRegistry.Get("MYWIDGET") -> WidgetDefinition
2. template loading: GetTemplateFullBSON(widgetID, idGenerator, projectPath)
a. Load json from embed.FS (or .mxcli/widgets/)
b. Augment from project's .mpk (if newer version available)
c. Phase 1: Collect all $ID values -> generate new UUID mapping
d. Phase 2: Convert type json -> BSON, extract PropertyTypeIDMap
e. Phase 3: Convert object json -> BSON (TypePointer remapped via same mapping)
f. placeholder leak check (aa000000-prefix IDs must all be remapped)
3. Mode selection: evaluateCondition() on each mode in order -> first match wins
4. Property mappings: for each mapping, resolveMapping() -> OperationFunc()
Each operation locates the WidgetProperty by matching TypePointer against PropertyTypeIDMap
5. Child slots: group AST children by container name, build to BSON, embed via opWidgets
6. Assemble customwidget{RawType, RawObject, PropertyTypeIDMap, ObjectTypeID}The map links PropertyKey names (from .def.json) to their BSON IDs:
PropertyTypeIDMap["datasource"] = {
PropertyTypeID: "a1b2c3d4...", // $ID of WidgetPropertyType in type
ValueTypeID: "e5f6a7b8...", // $ID of ValueType within PropertyType
DefaultValue: "",
ValueType: "datasource", // type string
ObjectTypeID: "...", // for nested object list properties
}Operations use this map to locate the correct WidgetProperty in the Object's Properties array by comparing TypePointer (binary GUID) against PropertyTypeID.
At template load time, augmentFromMPK() checks if the project has a newer .mpk for the widget:
project/widgets/*.mpk -> FindMPK(projectDir, widgetID) -> ParseMPK()
-> AugmentTemplate(clone, mpkDef)
-> add missing properties from newer .mpk version
-> remove stale properties no longer in .mpkThis reduces CE0463 errors from widget version drift without requiring manual template re-extraction.
| Priority | Location | Scope |
|---|---|---|
| 1 (highest) | <project>/.mxcli/widgets/*.def.json | Project |
| 2 | ~/.mxcli/widgets/*.def.json | Global (user) |
| 3 (lowest) | sdk/widgets/definitions/*.def.json (embedded) | Built-in |
Higher priority definitions override lower ones with the same MDL name (case-insensitive).
# list registered widgets
mxcli widget list -p App.mpr
# check after creating a page
mxcli check script.mdl -p App.mpr --references
# full mx check (catches CE0463)
mxcli docker check -p App.mpr
# debug CE0463 -- compare NDSL dumps
mxcli bson dump -p App.mpr --type page --object "Module.PageName" --format ndsl| Mistake | Fix |
|---|---|
| CE0463 after page creation | Template version mismatch -- extract fresh template from Studio Pro MPR, or ensure .mpk augmentation picks up new properties |
| Widget not recognized | Check mxcli widget list; .def.json must be in .mxcli/widgets/ with .def.json extension |
| TEMPLATE content missing | Widget needs childSlots entry with "mdlContainer": "template" |
| Association COMBOBOX shows enum behavior | Add datasource to trigger association mode (hasDataSource condition) |
| Association mapping fails | Ensure DataSource mapping appears before Association mapping in the array |
| Custom widget not found | Place .def.json in .mxcli/widgets/ inside the project directory |
| Placeholder ID leak error | Template JSON has unreferenced $ID values starting with aa000000 -- ensure all IDs are in the collectIDs traversal path |
| File | Purpose |
|---|---|
mdl/executor/widget_engine.go | PluggableWidgetEngine, 6 operations, Build() pipeline |
mdl/executor/widget_registry.go | 3-tier WidgetRegistry, definition validation |
sdk/widgets/loader.go | Template loading, ID remapping, MPK augmentation |
sdk/widgets/mpk/mpk.go | .mpk ZIP parsing, XML property extraction |
cmd/mxcli/cmd_widget.go | mxcli widget extract/list CLI commands |
sdk/widgets/definitions/*.def.json | Built-in widget definitions (ComboBox, Gallery) |
sdk/widgets/templates/mendix-11.6/*.json | Embedded BSON templates |
mdl/executor/cmd_pages_builder_input.go | updateWidgetPropertyValue() -- TypePointer matching |
© mendixlabs, Apache-2.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
Just SKILL.md in .claude/skills/mendix/custom-widgets of mendixlabs/mxcli.
Open the folder on GitHubat commit a924d11
Custom Widgets next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Custom Widgets this skillmendixlabs/mxcli | 128 | — | ~7.8k | Automated safety check: Pass | Apache-2.0 | |
| Makepad Widgetssickn33/agentic-awesome-skills | 47k | 2 repos | ~1.7k | Automated safety check: Pass | MIT | |
| Contact Widgetnexu-io/open-design | 100k | — | ~1.6k | Automated safety check: Pass | Apache-2.0 | |
| WidgetLeoYeAI/openclaw-master-skills | 2.2k | — | ~1.9k | Automated safety check: Pass | MIT | |
| Chat Widgetsickn33/agentic-awesome-skills | 47k | 2 repos | ~332 | Automated safety check: Pass | MIT | |
| Robius Widget Patternssickn33/agentic-awesome-skills | 47k | 2 repos | ~3.1k | Automated safety check: Pass | MIT |
sickn33/agentic-awesome-skills
Version: makepad-widgets (dev branch) | Last Updated: 2026-01-19 Check for updates: https://crates.io/crates/makepad-widgets
nexu-io/open-design
Self-contained floating chat widget with welcome screen, social links, meeting button, and message input.
LeoYeAI/openclaw-master-skills
Create, update, hide, show, list, and delete Übersicht desktop widgets on macOS.
sickn33/agentic-awesome-skills
Build a real-time support chat system with a floating widget for users and an admin dashboard for support staff.
sickn33/agentic-awesome-skills
CRITICAL: Use for Robius widget patterns. An agent skill from sickn33/agentic-awesome-skills.
sickn33/agentic-awesome-skills
Web and App implementation guide for Widget-Based Design. An agent skill from sickn33/agentic-awesome-skills.
mendixlabs/mxcli
Push OData query options into the SQL of a Mendix resource served by a read microflow, so $filter, $orderby, $top, $skip, $count and the key lookup reach the database instead of being silently…
mendixlabs/mxcli
Chart a Mendix app with Vega-Lite through a pluggable widget that takes the specification and the data as separate properties, so the model emits rows and never assembles a chart payload.
mendixlabs/mxcli
Author Mendix AI agent documents in MDL — Model, Knowledge Base, Consumed MCP Service and Agent, with variables, tools and multi-line prompts.
mendixlabs/mxcli
Run set-based INSERT, UPDATE and DELETE against Mendix entities through OQL statements, which the runtime supports and Studio Pro cannot author.
mendixlabs/mxcli
Stand up an HTTP endpoint you control instead of a live third-party API, and point the Mendix app at it — Prism from an OpenAPI contract, a constant swap, or a forward proxy.
mendixlabs/mxcli
Call external REST APIs from Mendix — the three approaches (inline REST CALL, consumed REST client document, generated from OpenAPI) and how to choose.
MDL syntax for pluggable widgets in CREATE PAGE / ALTER PAGE — any installed widget is named by its own name (htmlelement frame (…) { … }), with object lists and child slots read from its definition. Custom Widgets is an agent skill from mendixlabs/mxcli. MDL syntax for pluggable widgets in CREATE PAGE / ALTER PAGE — any installed widget is named by its own name (htmlelement frame (…) { … }), with object lists and child slots read from its definition.
Custom Widgets fits situations like: placing a pluggable widget on a page; mxcli widget describe output needs interpreting.
Run `npx skills add mendixlabs/mxcli --skill custom-widgets -a claude-code`. Or copy the skill folder (.claude/skills/mendix/custom-widgets in mendixlabs/mxcli) into .claude/skills/custom-widgets in your project. Claude Code loads it when a task matches its description.
Run `npx skills add mendixlabs/mxcli --skill custom-widgets -a codex`. Or copy the skill folder (.claude/skills/mendix/custom-widgets in mendixlabs/mxcli) into .agents/skills/custom-widgets in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add mendixlabs/mxcli --skill custom-widgets -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/custom-widgets, .gemini/skills/custom-widgets, .github/skills/custom-widgets and .opencode/skills/custom-widgets in your project.
SKILL.md names no scripts, command-line tools or credentials: Custom Widgets is instructions for the agent only.
SKILL.md contains no URLs. Any network use would come from the scripts or tools the agent runs. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Custom Widgets is published under the Apache-2.0 licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 7.8k tokens (SKILL.md is roughly 31k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Custom Widgets: Makepad Widgets (sickn33/agentic-awesome-skills, 47k stars), Contact Widget (nexu-io/open-design, 100k stars), Widget (LeoYeAI/openclaw-master-skills, 2.2k stars) and Chat Widget (sickn33/agentic-awesome-skills, 47k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
mendixlabs (a GitHub organization) maintains it in mendixlabs/mxcli, which has 128 GitHub stars. The repository holds 75 skills in this directory. The repository was last updated on October 7, 2026.
Source: mendixlabs/mxcli on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.