Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "sdk/javaactions", "date": "2026-10-16", "symptom": "A `create javascript action` whose body uses its own parameters (`Url`, `ReadOnly`) builds cleanly and throws `ReferenceError: Url is not defined` the first time the action runs. `mxcli check` and `mx check` report nothing. After a build the .js signature reads `JS_Open(url, readOnly)` where mxcli wrote `JS_Open(Url, ReadOnly)`, body untouched. A Studio Pro-authored action edited through `create or modify` turns `Geocode(address, ...)` into `Geocode(Address, ...)` over a body that still reads `address`", "cause": "`GenerateJavaScriptSource` wrote the model parameter name verbatim in the signature and the JSDoc `@param`. mxbuild and Studio Pro regenerate the file from the model, keeping only the user code, EXTRA CODE and imports, and lower-case the first character of each parameter name (only the first: `URL` -> `uRL`, `MyID` -> `myID`, `_Lead` unchanged), so the body was written against identifiers the next build removes", "file": "`sdk/javaactions/jssource.go` (`GenerateJavaScriptSource`, `JavaScriptParameterName`), goldens `sdk/javaactions/testdata/mxbuild/JS_*.js.golden`", "insight": "Generated source that a later tool regenerates must follow that tool's naming rule, or the user code inside it breaks silently: it is the user's, so the generator cannot fix it afterwards and nothing between MDL and the click can see it. Measure the rule against the real generator, not against the docs: create an action with awkward names (`URL, MyID, Some_Name, A, XMLData, _Lead, Obj2Name, Eid`), run mxbuild, read what it left (`uRL, myID, some_Name, a, xMLData, _Lead, obj2Name, eid`) - 'camelCase it' would have been wrong for `URL`. Keep the model name (the database keys on it; describe must round-trip) and change only the generated identifier. The earlier finding for the theme switcher worked around this by writing lower-case bodies; the generator was still wrong. Bodies are not rewritten, so a body that uses the declared case still fails: a `check` warning is the separate follow-up. mxbuild also rejects names that differ only in case (`Duplicate parameter name`), which mxcli does not check", "test": "TestJavaScriptParameterName_MatchesMxbuild, TestGenerateJavaScriptSource_SignatureMatchesMxbuild, TestWriteJavaScriptSourceFile_RewriteKeepsStudioProSignature", "refs": ["mendixlabs/mxcli#1560"]}
40 changes: 25 additions & 15 deletions .claude/skills/mendix/javascript-actions/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ description: "Create, call and drop JavaScript actions — client-side logic cal
|---|---|
| 31-39 | When to Use This Skill |
| 40-51 | Overview |
| 52-85 | Syntax |
| 86-147 | Examples |
| 148-159 | Calling from a nanoflow |
| 160-184 | The generated .js file |
| 185-193 | Inspecting |
| 194-202 | Validation Checklist |
| 203-211 | Common Errors |
| 212-216 | Related Documentation |
| 52-94 | Syntax |
| 95-156 | Examples |
| 157-168 | Calling from a nanoflow |
| 169-193 | The generated .js file |
| 194-202 | Inspecting |
| 203-211 | Validation Checklist |
| 212-221 | Common Errors |
| 222-226 | Related Documentation |

<!-- /skill-index -->
# Mendix JavaScript Actions Skill
Expand Down Expand Up @@ -83,6 +83,15 @@ Same type system as Java actions:
bare `pEntity` for instance parameters
- Append `NOT NULL` to mark a parameter required.

### Parameter names in the body

The body sees each parameter with its **first character lower-cased**: `A` is
`a`, `ReadOnly` is `readOnly`, `URL` is `uRL` (only the first character changes),
`_Lead` stays `_Lead`. That is the signature Studio Pro and mxbuild generate, and
mxcli writes it. The model keeps the declared name and `describe` prints it as
declared. A body that uses the declared spelling throws `ReferenceError` at run
time and nothing in `check` or the build reports it.

## Examples

Simple action (Web default):
Expand All @@ -104,7 +113,7 @@ CREATE JAVASCRIPT ACTION MyFirstModule.JSA_Add (
B: Integer NOT NULL
) RETURNS Integer
AS $$
return Promise.resolve(A + B);
return Promise.resolve(a + b);
$$;
```

Expand All @@ -119,7 +128,7 @@ CREATE JAVASCRIPT ACTION MyFirstModule.JSA_ShowToast (
EXPOSED AS 'Show Toast' IN 'UI'
PLATFORM Native
AS $$
console.log(Message);
console.log(message);
return Promise.resolve(true);
$$;
```
Expand All @@ -134,7 +143,7 @@ CREATE OR MODIFY JAVASCRIPT ACTION MyFirstModule.JSA_Add (
C: Integer
) RETURNS Integer
AS $$
return Promise.resolve(A + B + (C || 0));
return Promise.resolve(a + b + (c || 0));
$$;
```

Expand Down Expand Up @@ -168,13 +177,13 @@ This is the nanoflow counterpart of `CALL JAVA ACTION` (microflows). Use it in a
// END EXTRA CODE

/**
* @param {number} A
* @param {number} B
* @param {number} a
* @param {number} b
* @returns {Promise.<number>}
*/
export async function JSA_Add(A, B) {
export async function JSA_Add(a, b) {
// BEGIN USER CODE
return Promise.resolve(A + B);
return Promise.resolve(a + b);
// END USER CODE
}
```
Expand Down Expand Up @@ -208,6 +217,7 @@ DESCRIBE JAVASCRIPT ACTION Module.Name; -- re-executable MDL (signature + body
| `already exists — use create or modify to overwrite` | Duplicate name without `OR MODIFY` | Add `OR MODIFY` |
| Action runs nowhere / wrong client | `PLATFORM` too narrow | Set `PLATFORM All` (or the right target) |
| `mismatched input 'platform'` | Clause out of order | Put `platform` after `exposed as`, before `as` |
| `ReferenceError: Url is not defined` when the action runs | The body uses the declared parameter name; the `.js` signature has the first character lower-cased | Write `url`, not `Url` |

## Related Documentation

Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Fixed

- **A JavaScript action's parameters are written with the names mxbuild generates** (mendixlabs/mxcli#1560) — `create javascript action … (Url: String, ReadOnly: Boolean)` wrote `export async function JS_Open(Url, ReadOnly)`, while mxbuild and Studio Pro regenerate the signature with the first character of each parameter lower-cased, `JS_Open(url, readOnly)`, and keep the body. A body using the declared `Url` then threw `ReferenceError` the first time the action ran, with `check`, `mx check` and the build all clean. The signature and the JSDoc `@param` names now carry that name (only the first character changes: `URL` is `uRL`, `MyID` is `myID`, `_Lead` stays), measured against mxbuild 11.12.5, so what `exec` writes is what a build leaves. The same rewrite used to break a Studio Pro-authored action on its first edit through MDL: `Geocode(address, …)` became `Geocode(Address, …)` over a body that still read `address`. The model keeps the declared parameter names, `describe` prints them as declared, and bodies are not rewritten, so a body reads `readOnly`, not `ReadOnly`.

## [0.27.0] - 2026-10-15

Headline: **`exec` is all or nothing: when a statement fails at write time, the whole run is rolled back, so a script never leaves a project half-changed.** Alongside it: a wave of `describe` → `check` → `exec` fixes from round-trip runs over real apps (the Siemens FSM template, the GenAI showcase, Marketplace modules), Mendix 9 projects can take microflows, nanoflows and pluggable widgets again, `check -p` resolves many more of the references `exec` and the build would refuse, the classic `filemanager` widget and role-based `Visible: roles (…)` are in MDL, and the companion draws a module's domain model. The MongoDB driver is upgraded for the October 2026 BSON advisories.
Expand Down
2 changes: 1 addition & 1 deletion cmd/mxcli/syntax/features_integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -658,7 +658,7 @@ DESCRIBE DATABASE CONNECTION Ops.Erp;`,
"javascript action", "javascript", "js action", "call javascript",
"platform", "exposed as", "javascriptaction",
},
Syntax: "LIST JAVASCRIPT ACTIONS [IN Module];\nDESCRIBE JAVASCRIPT ACTION Module.Name;\nCREATE [OR MODIFY] JAVASCRIPT ACTION Module.Name [FOLDER 'path'](...) RETURNS Type [PLATFORM Web|Native|Hybrid|All] AS $$ ... $$;\nDROP JAVASCRIPT ACTION Module.Name;\n\nWrites the unit plus javascriptsource/<Module>/actions/<Name>.js. AS $$ ... $$ is mandatory. PLATFORM defaults to Web.",
Syntax: "LIST JAVASCRIPT ACTIONS [IN Module];\nDESCRIBE JAVASCRIPT ACTION Module.Name;\nCREATE [OR MODIFY] JAVASCRIPT ACTION Module.Name [FOLDER 'path'](...) RETURNS Type [PLATFORM Web|Native|Hybrid|All] AS $$ ... $$;\nDROP JAVASCRIPT ACTION Module.Name;\n\nWrites the unit plus javascriptsource/<Module>/actions/<Name>.js. AS $$ ... $$ is mandatory. PLATFORM defaults to Web. The body sees each parameter with its first character lower-cased (ReadOnly is readOnly, URL is uRL), as mxbuild generates the signature.",
Example: "mdl 1;\nCREATE JAVASCRIPT ACTION Utils.IsStrictMode() RETURNS Boolean\nPLATFORM Web\nAS $$\nreturn Promise.resolve((function(){ return !this; })());\n$$;\n\n-- Exposed, native, with parameters\nCREATE JAVASCRIPT ACTION Utils.ShowToast(\n Message: String NOT NULL,\n Duration: Integer\n) RETURNS Boolean\nEXPOSED AS 'Show Toast' IN 'UI'\nPLATFORM Native\nAS $$\nreturn Promise.resolve(true);\n$$;",
SeeAlso: []string{"java-action"},
})
Expand Down
16 changes: 13 additions & 3 deletions docs-site/src/reference/microflow/create-javascript-action.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ If `OR MODIFY` is specified and the action already exists, its parameters, retur
type, exposed-as settings, platform, and body are updated in place; the UUID is
preserved.

### Parameter names in the body

The model keeps a parameter's name as declared, but the generated `.js` file uses
the name Studio Pro and mxbuild generate: the **first character lower-cased**,
nothing else. The body is written against that name. `ReadOnly` is `readOnly`,
`Url` is `url`, `URL` is `uRL` (not `url`), `MyID` is `myID`, `_Lead` stays
`_Lead`. mxcli writes the signature this way and does not rewrite the body, so a
body that uses the declared spelling (`ReadOnly`) throws a `ReferenceError` when
the action runs. `DESCRIBE` still prints the declared names.

### Platform

The optional `PLATFORM` clause restricts where the action runs: `Web`, `Native`,
Expand Down Expand Up @@ -96,7 +106,7 @@ CREATE JAVASCRIPT ACTION MyModule.JSA_Add (
B: Integer NOT NULL
) RETURNS Integer
AS $$
return Promise.resolve(A + B);
return Promise.resolve(a + b);
$$;
```

Expand All @@ -110,7 +120,7 @@ CREATE JAVASCRIPT ACTION MyModule.JSA_ShowToast (
EXPOSED AS 'Show Toast' IN 'UI'
PLATFORM Native
AS $$
console.log(Message);
console.log(message);
return Promise.resolve(true);
$$;
```
Expand All @@ -124,7 +134,7 @@ CREATE OR MODIFY JAVASCRIPT ACTION MyModule.JSA_Add (
C: Integer
) RETURNS Integer
AS $$
return Promise.resolve(A + B + (C || 0));
return Promise.resolve(a + b + (c || 0));
$$;
```

Expand Down
68 changes: 68 additions & 0 deletions mdl-examples/bug-tests/1560-js-action-parameter-case.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
mdl 1;
-- ============================================================================
-- Bug: a JavaScript action's body threw ReferenceError for its own parameters
-- ============================================================================
--
-- Symptom
-- An action declared with `Url: String, ReadOnly: Boolean` and a body that
-- uses `Url` and `ReadOnly` was written with
--
-- export async function JS_Open(Url, ReadOnly)
--
-- `mxcli check` passed and so did `mx check`. After a build, mxbuild had
-- regenerated the signature as `JS_Open(url, readOnly)` and kept the body, so
-- `Url` and `ReadOnly` no longer existed and the action threw a
-- ReferenceError when a user clicked it. Nothing warned. The same happened to
-- an action authored in Studio Pro the first time it was changed through
-- `create or modify`: `Geocode(address, ...)` became `Geocode(Address, ...)`
-- with a body that still read `address`.
--
-- Cause
-- mxbuild and Studio Pro lower-case the FIRST character of each parameter name
-- in the generated .js (signature and JSDoc @param); the model keeps the
-- declared name. mxcli wrote the declared name, so the file it wrote was not
-- the file the build produces.
--
-- Fix
-- mxcli writes the name mxbuild writes: only the first character is
-- lower-cased (URL -> uRL, MyID -> myID, _Lead -> _Lead). The model parameter
-- keeps its declared name, so `describe` prints it as declared. Bodies are not
-- rewritten: a body sees the lower-first name.
--
-- Verify
-- mxcli exec mdl-examples/bug-tests/1560-js-action-parameter-case.mdl -p app.mpr
--
-- Then the signature in the file is what a build will produce:
--
-- grep -q "export async function JsParamCase_Open(url, readOnly)" \
-- javascriptsource/jsparamcase/actions/JsParamCase_Open.js
--
-- and `describe javascript action JsParamCase.JsParamCase_Open` still prints
-- `Url: String` and `ReadOnly: Boolean`.
--
-- Running mxbuild on the project must leave that signature as it is; before
-- the fix it rewrote it to lower-first and broke the body.
-- ============================================================================

create module JsParamCase;

create or modify javascript action JsParamCase.JsParamCase_Open(
Url: String,
ReadOnly: Boolean
) returns String
as $$
// The body uses the names mxbuild generates: first character lower-cased.
return Promise.resolve(url + (readOnly ? " (read only)" : ""));
$$;

create or modify javascript action JsParamCase.JsParamCase_Names(
URL: String,
MyID: String,
Some_Name: String,
XMLData: String,
_Lead: String,
Obj2Name: String
) returns String
as $$
return Promise.resolve(uRL + myID + some_Name + xMLData + _Lead + obj2Name);
$$;
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ create or modify javascript action JsDirCase.JsDirCase_Echo(
as $$
// MXCLI_MARKER — if this text is missing from the deployed bundle, the source
// was written to a directory Mendix does not read.
return Promise.resolve(Value);
return Promise.resolve(value);
$$;

create or modify nanoflow JsDirCase.ACT_Echo()
Expand Down
10 changes: 5 additions & 5 deletions mdl-examples/doctype-tests/07b-javascript-action-examples.mdl
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ create javascript action JsTest.Add(
B: Integer not null
) returns Integer
as $$
return Promise.resolve(A + B);
return Promise.resolve(a + b);
$$;

-- MARK: Exposed (toolbox) action targeting native
Expand All @@ -51,7 +51,7 @@ create javascript action JsTest.ShowToast(
exposed as 'Show Toast' in 'UI'
platform Native
as $$
console.log(Message);
console.log(message);
return Promise.resolve(true);
$$;

Expand All @@ -63,7 +63,7 @@ create or modify javascript action JsTest.Add(
C: Integer
) returns Integer
as $$
return Promise.resolve(A + B + (C || 0));
return Promise.resolve(a + b + (c || 0));
$$;

-- MARK: Enumeration parameter (issue #680)
Expand All @@ -82,14 +82,14 @@ create javascript action JsTest.FormatSeverity(
Level: Enumeration(JsTest.Severity) not null
) returns String
as $$
return Promise.resolve(String(Level));
return Promise.resolve(String(level));
$$;

create javascript action JsTest.NextSeverity(
Current: ENUM JsTest.Severity not null
) returns Enumeration(JsTest.Severity)
as $$
return Promise.resolve(Current);
return Promise.resolve(current);
$$;

-- MARK: Inspect
Expand Down
10 changes: 5 additions & 5 deletions mdl-examples/doctype-tests/37-theme-switcher-examples.mdl
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,15 @@ as $$
var root = document.documentElement;
root.classList.remove("theme-light", "theme-dark");
try {
if (Theme === "light" || Theme === "dark") {
root.classList.add("theme-" + Theme);
window.localStorage.setItem("mxcli-theme", Theme);
if (theme === "light" || theme === "dark") {
root.classList.add("theme-" + theme);
window.localStorage.setItem("mxcli-theme", theme);
} else {
window.localStorage.removeItem("mxcli-theme");
}
} catch (e) {
if (Theme === "light" || Theme === "dark") {
root.classList.add("theme-" + Theme);
if (theme === "light" || theme === "dark") {
root.classList.add("theme-" + theme);
}
}
return Promise.resolve(true);
Expand Down
2 changes: 1 addition & 1 deletion mdl-examples/doctype-tests/39-expose-as-toolbox-action.mdl
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ exposed as 'Slugify' in 'Toolbox demo'
icon 'assets/toolbox-icon-64.png'
platform Web
as $$
return Promise.resolve(String(Input).toLowerCase().replace(/[^a-z0-9]+/g, '-'));
return Promise.resolve(String(input).toLowerCase().replace(/[^a-z0-9]+/g, '-'));
$$;

-- =============================================================================
Expand Down
82 changes: 82 additions & 0 deletions mdl/backend/modelsdk/javascript_write_retain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,85 @@ func TestWriteJavaScriptSourceFile_NewFileHasEmptySections(t *testing.T) {
t.Errorf("new stub has unexpected sections:\n%s", got)
}
}

// mxbuild and Studio Pro regenerate a JavaScript action's signature with the
// first character of each parameter name lower-cased and keep the body, so the
// file mxcli writes has to carry those names or the body is written against
// identifiers that stop existing at the next build.
func TestWriteJavaScriptSourceFile_ParametersAreLowerFirst(t *testing.T) {
dir := t.TempDir()
b := &Backend{path: filepath.Join(dir, "App.mpr")}
params := []*types.JavaActionParameter{
{Name: "Url", ParameterType: &types.StringType{}},
{Name: "ReadOnly", ParameterType: &types.BooleanType{}},
}
if err := b.WriteJavaScriptSourceFile("Demo", "JS_Open", `return url + (readOnly ? " (read only)" : "");`, params, &types.StringType{}); err != nil {
t.Fatal(err)
}
got, err := os.ReadFile(filepath.Join(b.jsActionSourceDir("Demo"), "JS_Open.js"))
if err != nil {
t.Fatal(err)
}
for _, want := range []string{
" * @param {string} url\n",
" * @param {boolean} readOnly\n",
"export async function JS_Open(url, readOnly) {\n",
} {
if !strings.Contains(string(got), want) {
t.Errorf("source lacks %q:\n%s", want, got)
}
}
}

// Rewriting an action authored in Studio Pro must reproduce the signature it
// has: the model declares Address, the file says address, and the body uses
// address. Before, the rewrite turned the signature into Address while the body
// kept address.
func TestWriteJavaScriptSourceFile_RewriteKeepsStudioProSignature(t *testing.T) {
const stored = `// This file was generated by Mendix Studio Pro.
import { Big } from "big.js";

// BEGIN EXTRA CODE
// END EXTRA CODE

/**
* @param {string} [address]
* @param {string} [geocodingProvider]
* @param {string} [providerApiKey]
* @returns {Promise.<string>}
*/
export async function Locate(address, geocodingProvider, providerApiKey) {
// BEGIN USER CODE
return address + geocodingProvider;
// END USER CODE
}
`
dir := t.TempDir()
b := &Backend{path: filepath.Join(dir, "App.mpr")}
jsDir := b.jsActionSourceDir("Demo")
if err := os.MkdirAll(jsDir, 0o755); err != nil {
t.Fatal(err)
}
path := filepath.Join(jsDir, "Locate.js")
if err := os.WriteFile(path, []byte(stored), 0o644); err != nil {
t.Fatal(err)
}
params := []*types.JavaActionParameter{
{Name: "Address", ParameterType: &types.StringType{}},
{Name: "GeocodingProvider", ParameterType: &types.StringType{}},
{Name: "ProviderApiKey", ParameterType: &types.StringType{}},
}
if err := b.WriteJavaScriptSourceFile("Demo", "Locate", "return address + geocodingProvider + providerApiKey;", params, &types.StringType{}); err != nil {
t.Fatal(err)
}
got, _ := os.ReadFile(path)
const sig = "export async function Locate(address, geocodingProvider, providerApiKey) {\n"
if !strings.Contains(string(got), sig) {
t.Errorf("rewrite changed the Studio Pro signature:\n%s", got)
}
for _, name := range []string{"address", "geocodingProvider", "providerApiKey"} {
if !strings.Contains(string(got), " "+name+"\n") {
t.Errorf("JSDoc lost @param %s:\n%s", name, got)
}
}
}
Loading
Loading