Skip to content
Merged
20 changes: 20 additions & 0 deletions .claude/skills/mendix/write-microflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -885,6 +885,26 @@ download file $GeneratedReport show in browser;
download file $GeneratedExport;
```

## Empty Java-Action Argument (`empty`)

When `describe` round-trips a Java-action call that has an unbound parameter
in Studio Pro, it emits `empty` as the argument value. In this Java-action
argument context, `empty` preserves the
underlying empty `BasicCodeActionParameterValue.Argument` so that the next
`describe → exec → describe` cycle stays symmetric.

```mdl
$Total = call java action SampleModule.Recalculate(
CompanyId = empty,
RecalculateAll = true,
ItemList = empty
);
```

New scripts should bind every parameter to a real expression. Use `empty`
for a Java-action argument only when regenerating MDL from an existing project
that already had an unbound parameter.

## Error Handling

MDL supports error handling for activities that may fail (microflow calls, commits, external service calls, etc.).
Expand Down
1 change: 1 addition & 0 deletions docs/01-project/MDL_QUICK_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,7 @@ Module.OrderResponse_CustomerInfo/Module.CustomerInfo as customer {
| Rename Java action (dry run) | `rename java action Module.Old to New dry run;` | Preview reference changes without modifying |
| Drop Java action | `drop java action Module.Name;` | Deletes MPR unit and .java source file |
| Call from microflow | `$Result = call java action Module.Name(Param = value);` | Inside BEGIN...END |
| Empty argument | `call java action Module.Name(Param = empty);` | Unbound code-action parameter preserved as empty mapping |

**Parameter Types:** `string`, `integer`, `long`, `decimal`, `boolean`, `datetime`, `Module.Entity`, `list of Module.Entity`, `enum Module.EnumName`, `enumeration(Module.EnumName)`, `stringtemplate(sql)`, `stringtemplate(Oql)`, `entity <pEntity>` (type parameter declaration), bare `pEntity` (type parameter reference).

Expand Down
72 changes: 72 additions & 0 deletions docs/11-proposals/PROPOSAL_microflow_empty_java_action_argument.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Empty Java Action Argument

Status: Implemented

## Summary

Use the existing MDL `empty` literal to represent an intentionally unbound Java
action argument in microflow call statements.

```mdl
$Total = call java action SampleModule.Recalculate(
CompanyId = empty,
RecalculateAll = true,
ItemList = empty
);
```

In this Java-action argument context, `empty` produces a parameter binding with
an empty `Argument` string in the serialized BSON
(`Microflows$BasicCodeActionParameterValue.Argument = ""`). Re-executing the
script reproduces the same empty binding, so `describe -> exec -> describe`
stays symmetric for existing Studio Pro projects that have unbound code-action
parameters.

## Motivation

Studio Pro's Java-action call dialog allows a developer to leave individual
parameters empty. The on-disk representation is a
`Microflows$JavaActionParameterMapping` whose value is a
`BasicCodeActionParameterValue` with `Argument: ""`.

Emitting `''` would create a literal empty string expression, not an unbound
parameter. Dropping the parameter would lose the original mapping. The existing
`empty` literal is already valid MDL expression syntax and is clearer than
introducing a new placeholder token for this one case.

## Semantics

- In Java-action call arguments, `empty` maps to an empty
`BasicCodeActionParameterValue.Argument`.
- If the Java action parameter type is a microflow callback, `empty` maps to a
`Microflows$MicroflowParameterValue` with an empty `Microflow` reference.
- Outside Java-action call arguments, `empty` keeps its normal MDL literal
meaning.

## Examples

```mdl
-- Java action call with two unbound and one bound argument.
$Total = call java action SampleModule.Recalculate(
CompanyId = empty,
RecalculateAll = true,
ItemList = empty
);
```

The Mendix BSON for the unbound arguments is:

```text
JavaActionParameterMapping {
Parameter: 'SampleModule.Recalculate.CompanyId',
Value: BasicCodeActionParameterValue { Argument: '' }
}
```

## Tests And Examples

- Builder coverage: `TestBuildJavaAction_EmptyArgumentPreservesEmptyBasicValue`
and `TestBuildJavaAction_EmptyMicroflowArgumentUsesMicroflowParameterValue`
in `mdl/executor/cmd_microflows_builder_java_action_test.go`.
- Example script:
`mdl-examples/doctype-tests/empty_java_action_argument.mdl`.
1 change: 1 addition & 0 deletions docs/11-proposals/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ BSON schema Registry ◄──── multi-version Support
| [MDL Syntax Improvements v2](PROPOSAL_mdl_syntax_improvements_v2.md) | Proposed | Consolidated v2: unified variable declaration, C-style braces, fluent list ops | Syntax Improvements v1 |
| [Microflow Free Annotation](PROPOSAL_microflow_free_annotation.md) | Draft | Order-sensitive `@annotation` handling for free-floating visual notes in microflows | — |
| [Microflow Download File Statement](PROPOSAL_microflow_download_file_statement.md) | Draft | `download file $FileDocument [show in browser]` for `DownloadFileAction` round-trip and authoring | — |
| [Empty Java Action Argument](PROPOSAL_microflow_empty_java_action_argument.md) | Implemented | `empty` for unbound Java-action parameters; round-trip preservation of empty `BasicCodeActionParameterValue` bindings | — |
| [Microflow Call Web Service Statement](PROPOSAL_microflow_call_web_service_statement.md) | Draft | Structured and raw MDL syntax for legacy SOAP `CallWebServiceAction` round-trip preservation | — |
| [Page Syntax V2](PROPOSAL_page_syntax_v2.md) | Superseded | Page/widget syntax with `{}` blocks and `->` binding. Superseded by V3 (archived) | — |
| [Page Styling Support](page-styling-support.md) | Partial | CSS classes, inline styles, dynamic classes, design properties. Phase 1 (Class/Style) done | — |
Expand Down
25 changes: 25 additions & 0 deletions mdl-examples/doctype-tests/empty_java_action_argument.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
create microflow SampleModule.ACT_RecalculateOpenItems ()
returns Void
begin
call java action SampleModule.Recalculate(
CompanyId = empty,
RecalculateAll = true,
ItemList = empty
);
return;
end;
/

create microflow SampleModule.ACT_RecalculateForCompany (
$CompanyId: String
)
returns Void
begin
call java action SampleModule.Recalculate(
CompanyId = $CompanyId,
RecalculateAll = false,
ItemList = empty
);
return;
end;
/
33 changes: 30 additions & 3 deletions mdl/executor/cmd_microflows_builder_calls.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,10 +245,13 @@ func (fb *flowBuilder) addCallJavaActionAction(s *ast.CallJavaActionStmt) model.

// Build a map of parameter name -> param type for the Java action
entityTypeParams := make(map[string]bool)
microflowTypeParams := make(map[string]bool)
if jaDef != nil {
for _, p := range jaDef.Parameters {
if _, ok := p.ParameterType.(*javaactions.EntityTypeParameterType); ok {
entityTypeParams[p.Name] = true
} else if _, ok := p.ParameterType.(*javaactions.MicroflowType); ok {
microflowTypeParams[p.Name] = true
}
}
}
Expand Down Expand Up @@ -277,12 +280,31 @@ func (fb *flowBuilder) addCallJavaActionAction(s *ast.CallJavaActionStmt) model.
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
Entity: entityName,
}
} else if isEmptyJavaActionArgument(arg.Value) {
if microflowTypeParams[arg.Name] {
value = &microflows.MicroflowParameterValue{
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
Microflow: "",
}
} else {
value = &microflows.BasicCodeActionParameterValue{
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
Argument: "",
}
}
} else {
// Regular parameter: expression-based value
valueExpr := fb.exprToString(arg.Value)
value = &microflows.BasicCodeActionParameterValue{
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
Argument: valueExpr,
if microflowTypeParams[arg.Name] {
value = &microflows.MicroflowParameterValue{
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
Microflow: strings.Trim(valueExpr, "'"),
}
} else {
value = &microflows.BasicCodeActionParameterValue{
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
Argument: valueExpr,
}
}
}

Expand Down Expand Up @@ -438,6 +460,11 @@ func (fb *flowBuilder) addCallJavaScriptActionAction(s *ast.CallJavaScriptAction
return activity.ID
}

func isEmptyJavaActionArgument(expr ast.Expression) bool {
lit, ok := expr.(*ast.LiteralExpr)
return ok && (lit.Kind == ast.LiteralEmpty || lit.Kind == ast.LiteralNull)
}

// addCallWebServiceAction creates a legacy SOAP WebServiceCallAction.
func (fb *flowBuilder) addCallWebServiceAction(s *ast.CallWebServiceStmt) model.ID {
activityX := fb.posX
Expand Down
116 changes: 116 additions & 0 deletions mdl/executor/cmd_microflows_builder_java_action_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// SPDX-License-Identifier: Apache-2.0

package executor

import (
"testing"

"github.com/mendixlabs/mxcli/mdl/ast"
"github.com/mendixlabs/mxcli/mdl/backend/mock"
"github.com/mendixlabs/mxcli/model"
"github.com/mendixlabs/mxcli/sdk/javaactions"
"github.com/mendixlabs/mxcli/sdk/microflows"
)

func TestBuildJavaAction_EmptyArgumentPreservesEmptyBasicValue(t *testing.T) {
fb := &flowBuilder{posX: 100, posY: 100, spacing: HorizontalSpacing}
stmt := &ast.CallJavaActionStmt{
ActionName: ast.QualifiedName{Module: "SampleModule", Name: "Recalculate"},
Arguments: []ast.CallArgument{
{Name: "CompanyId", Value: &ast.LiteralExpr{Kind: ast.LiteralEmpty}},
{Name: "RecalculateAll", Value: &ast.LiteralExpr{Kind: ast.LiteralBoolean, Value: true}},
{Name: "ItemList", Value: &ast.LiteralExpr{Kind: ast.LiteralEmpty}},
},
}

id := fb.addCallJavaActionAction(stmt)
var activity *microflows.ActionActivity
for _, obj := range fb.objects {
if obj.GetID() == id {
activity, _ = obj.(*microflows.ActionActivity)
break
}
}
if activity == nil {
t.Fatal("expected Java action activity")
}
action, ok := activity.Action.(*microflows.JavaActionCallAction)
if !ok {
t.Fatalf("action = %T, want *JavaActionCallAction", activity.Action)
}
if len(action.ParameterMappings) != 3 {
t.Fatalf("parameter mappings = %d, want 3", len(action.ParameterMappings))
}

for _, idx := range []int{0, 2} {
value, ok := action.ParameterMappings[idx].Value.(*microflows.BasicCodeActionParameterValue)
if !ok {
t.Fatalf("mapping %d value = %T, want *BasicCodeActionParameterValue", idx, action.ParameterMappings[idx].Value)
}
if value.Argument != "" {
t.Fatalf("mapping %d argument = %q, want empty string", idx, value.Argument)
}
}

value, ok := action.ParameterMappings[1].Value.(*microflows.BasicCodeActionParameterValue)
if !ok {
t.Fatalf("boolean mapping value = %T, want *BasicCodeActionParameterValue", action.ParameterMappings[1].Value)
}
if value.Argument != "true" {
t.Fatalf("boolean argument = %q, want true", value.Argument)
}
}

func TestBuildJavaAction_EmptyMicroflowArgumentUsesMicroflowParameterValue(t *testing.T) {
fb := &flowBuilder{
posX: 100,
posY: 100,
spacing: HorizontalSpacing,
backend: &mock.MockBackend{
ReadJavaActionByNameFunc: func(qualifiedName string) (*javaactions.JavaAction, error) {
if qualifiedName != "SampleModule.StartAsync" {
t.Fatalf("java action lookup = %q", qualifiedName)
}
return &javaactions.JavaAction{
Parameters: []*javaactions.JavaActionParameter{
{
Name: "Callback",
ParameterType: &javaactions.MicroflowType{
BaseElement: model.BaseElement{ID: "param-type"},
},
},
},
}, nil
},
},
}
stmt := &ast.CallJavaActionStmt{
ActionName: ast.QualifiedName{Module: "SampleModule", Name: "StartAsync"},
Arguments: []ast.CallArgument{
{Name: "Callback", Value: &ast.LiteralExpr{Kind: ast.LiteralEmpty}},
},
}

id := fb.addCallJavaActionAction(stmt)
var activity *microflows.ActionActivity
for _, obj := range fb.objects {
if obj.GetID() == id {
activity, _ = obj.(*microflows.ActionActivity)
break
}
}
if activity == nil {
t.Fatal("expected Java action activity")
}
action, ok := activity.Action.(*microflows.JavaActionCallAction)
if !ok {
t.Fatalf("action = %T, want *JavaActionCallAction", activity.Action)
}
value, ok := action.ParameterMappings[0].Value.(*microflows.MicroflowParameterValue)
if !ok {
t.Fatalf("mapping value = %T, want *MicroflowParameterValue", action.ParameterMappings[0].Value)
}
if value.Microflow != "" {
t.Fatalf("placeholder microflow = %q, want empty string", value.Microflow)
}
}
12 changes: 11 additions & 1 deletion mdl/executor/cmd_microflows_format_action.go
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,17 @@ func formatAction(
case *microflows.ExpressionBasedCodeActionParameterValue:
valueStr = v.Expression
case *microflows.BasicCodeActionParameterValue:
valueStr = v.Argument
if v.Argument == "" {
valueStr = "empty"
} else {
valueStr = v.Argument
}
case *microflows.MicroflowParameterValue:
if v.Microflow != "" {
valueStr = mdlQuote(v.Microflow)
} else {
valueStr = "empty"
}
case *microflows.EntityTypeCodeActionParameterValue:
if v.Entity != "" {
valueStr = mdlQuote(v.Entity)
Expand Down
26 changes: 26 additions & 0 deletions mdl/executor/cmd_microflows_format_action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,32 @@ func TestFormatAction_JavaActionCall(t *testing.T) {
}
}

func TestFormatAction_JavaActionCall_EmptyParameterValues(t *testing.T) {
e := newTestExecutor()
action := &microflows.JavaActionCallAction{
JavaAction: "MyModule.Recalculate",
ParameterMappings: []*microflows.JavaActionParameterMapping{
{
Parameter: "MyModule.Recalculate.CompanyId",
Value: &microflows.BasicCodeActionParameterValue{Argument: ""},
},
{
Parameter: "MyModule.Recalculate.RecalculateAll",
Value: &microflows.BasicCodeActionParameterValue{Argument: "true"},
},
{
Parameter: "MyModule.Recalculate.Callback",
Value: &microflows.MicroflowParameterValue{Microflow: ""},
},
},
}
got := e.formatAction(action, nil, nil)
want := "call java action MyModule.Recalculate(CompanyId = empty, RecalculateAll = true, Callback = empty);"
if got != want {
t.Errorf("got %q, want %q", got, want)
}
}

func TestFormatAction_CallExternal(t *testing.T) {
e := newTestExecutor()
action := &microflows.CallExternalAction{
Expand Down
Loading
Loading