java-actions · diff

git:20260822.b0cab65 to git:20260825.161b6eb

44 added, 0 removed. Audit A to A.

---
name: java-actions
description: "Create and call custom Java actions — extending Mendix with server-side Java, and invoking those actions from microflows in MDL. Use when logic needs a Java library, an algorithm microflows cannot express, or an integration only available in Java."
---
# Mendix Java Actions Skill
This skill provides comprehensive guidance for creating and using custom Java actions in Mendix projects.
## Reference files
`SKILL.md` covers declaring a Java action in MDL and calling it from a microflow —
the parts that touch the model. The Java side is next door:
- [`reference/writing-java.md`](reference/writing-java.md) — creating the action
in Studio Pro, writing the `executeAction` body, the Core API surface you have,
and the recurring patterns (HTTP calls, file handling, batch work).
- [`reference/best-practices.md`](reference/best-practices.md) — error handling,
transactions and rollback, logging, threading, security, and performance. Read
it before shipping a Java action that touches data or an external system.
## When to Use This Skill
Use this skill when:
- You need to extend Mendix with custom Java logic
- Building integrations with external Java libraries
- Implementing complex algorithms not feasible in microflows
- Calling Java actions from MDL microflows
- Debugging Java action calls
## Overview
Java actions allow you to extend Mendix with custom Java code. The workflow is:
1. **Define** the Java action in Studio Pro (parameters, return type)
2. **Implement** the Java code in Eclipse/IDE
3. **Call** the Java action from microflows using MDL
## Part 2.5: Creating Java Actions in MDL
MDL supports defining Java actions with inline Java code using `create java action`.
### Basic Syntax
```mdl
create java action Module.ActionName(param1: type, param2: type) returns ReturnType
as $$
// java code here
return result;
$$;
```
**`AS $$ ... $$` is mandatory.** The body cannot be omitted even for placeholder or stub actions. Omitting it causes a parse error: `no viable alternative at input '...'`. Use a minimal body if the real implementation is not yet written:
```mdl
create java action Module.Stub() returns boolean
as $$
return false;
$$;
```
### Type Parameters (Generics)
Type parameters let Java actions accept any entity type dynamically. Use `entity <pEntity>` in a parameter type to declare the type parameter inline. That parameter becomes the **entity type selector** (receives the entity type name, e.g., `'Module.Entity'`). Bare `pEntity` parameters become **parameterized entity** params (receive entity instances, e.g., `$Variable`).
```mdl
-- ENTITY <pEntity> declares the type parameter; bare pEntity references it
create java action Module.Validate(
EntityType: entity <pEntity> not null,
InputObject: pEntity not null
) returns boolean
as $$
return InputObject != null;
$$;
```
Multiple type parameters use separate `entity <...>` declarations:
```mdl
create java action Module.Transform(
SourceType: entity <pSource> not null,
TargetType: entity <pTarget> not null,
source: pSource not null,
Target: pTarget not null
) returns boolean
as $$
return true;
$$;
```
Type parameter names can be mixed with regular parameter types:
```mdl
create java action Module.CopyAttributes(
EntityType: entity <pEntity> not null,
source: pEntity not null,
Target: pEntity not null,
AttributeNames: string not null
) returns boolean
as $$
return true;
$$;
```
When **calling** these actions from microflows, the entity type selector receives the entity type name as a string literal, while instance params receive variables:
```mdl
$Result = call java action Module.CopyAttributes(
EntityType = 'Module.ProcessResult',
source = $source,
Target = $Target,
AttributeNames = 'Name,Status'
);
```
### EXPOSED AS (Toolbox Visibility)
The `exposed as 'caption' in 'Category'` clause makes the Java action appear as a toolbox item in Studio Pro's microflow editor:
```mdl
create java action Module.FormatCurrency(
Amount: decimal not null,
CurrencyCode: string not null
) returns string
exposed as 'Format Currency' in 'Formatting'
as $$
return String.format("%.2f %s", Amount, CurrencyCode);
$$;
```
Type parameters and EXPOSED AS can be combined:
```mdl
create java action Module.DeepClone(
EntityType: entity <pEntity> not null,
Original: pEntity not null
) returns boolean
exposed as 'Deep Clone Object' in 'Object Utils'
as $$
return true;
$$;
```
+ #### Toolbox icon and image
+
+ The entry also carries four PNG bitmaps — an icon and a larger image, each with
+ a dark-mode variant. Paths resolve against the working directory:
+
+ ```mdl
+ exposed as 'Format Currency' in 'Formatting'
+ icon 'assets/currency-64.png'
+ icon dark 'assets/currency-64-dark.png'
+ image 'assets/currency-256.png'
+ ```
+
+ The icon should be **64x64** and the image **256x192**. A different size is
+ written with a warning (Studio Pro scales it); a file that is not a PNG is
+ refused, because Studio Pro renders nothing for one.
+
+ #### An omitted clause preserves — it does not clear
+
+ This is the rule to remember, and it is not the obvious one:
+
+ | You write | What happens |
+ |---|---|
+ | `exposed as 'X' in 'Y'` | Caption and category from MDL; **icon and image carried** from what was stored |
+ | *(no clause at all)* | The whole entry is **preserved**, bitmaps included |
+ | `not exposed` | The entry is removed |
+ | `exposed as 'X' in 'Y' drop icon dark` | Clears **one** bitmap; the other three are untouched |
+
+ Rewriting a Java action to change its body must not cost it the icon a designer
+ set in Studio Pro, and saying nothing about the toolbox is not the same as
+ asking to be taken out of it. Nothing below Studio Pro can see the difference:
+ an action with no toolbox entry is a valid action, so `mx check` reports 0
+ errors either way.
+
+ `describe java action` reports the bitmaps as comments rather than re-emitting
+ `icon`/`image` clauses — the clause names a file on disk and the model holds
+ only bytes. They survive a describe → exec round trip regardless, because an
+ omitted bitmap is preserved.
+
+ **Microflows have this too**, and two of them: `exposed as microflow action` and
+ `exposed as workflow action`, for Studio Pro's two toolboxes. See
+ `write-microflows`. Nanoflows and rules do not — only `Microflows$Microflow`
+ stores a toolbox entry, and the clause is refused on the other two with a
+ message saying so.
+
### Supported Parameter Types
| MDL Type | Description |
|----------|-------------|
| `string` | Text value |
| `integer` | Whole number |
| `long` | Large whole number |
| `decimal` | Decimal number |
| `boolean` | True/false |
| `datetime` | Date and time |
| `Module.Entity` | Entity reference |
| `list of Module.Entity` | List of entities |
| `stringtemplate(sql)` | SQL/OQL query template with parameters |
| `stringtemplate(text)` | Text template with parameters |
| `entity <pEntity>` | Type parameter declaration (entity type selector) |
| `enum Module.EnumName` | Enumeration type |
| `enumeration(Module.EnumName)` | Enumeration type (alternative syntax) |
| `pEntity` (type param ref) | Type parameter reference (entity instance) |
### Examples
#### Simple Action (No Parameters)
```mdl
/** Returns the current timestamp. */
create java action MyModule.GetCurrentTimestamp() returns datetime
as $$
return new java.util.Date();
$$;
```
#### Action with Primitive Parameters
```mdl
/** Calculates tax amount. */
create java action Finance.CalculateTax(Amount: decimal, TaxRate: decimal) returns decimal
as $$
if (Amount == null || TaxRate == null) {
return java.math.BigDecimal.ZERO;
}
return Amount.multiply(TaxRate).divide(java.math.BigDecimal.valueOf(100), 2, java.math.RoundingMode.HALF_UP);
$$;
```
#### Action with StringTemplate (SQL/OQL)
```mdl
/** Executes an OQL statement with parameterized query. */
create java action Database.ExecuteOQLStatement(OqlStatement: stringtemplate(sql) not null) returns boolean
as $$
// execute the parameterized OQL statement
// The stringtemplate handles parameter substitution safely
return true;
$$;
```
#### Action with NOT NULL Parameter
```mdl
/** Validates an email address - email is required. */
create java action Validation.ValidateEmail(EmailAddress: string not null) returns boolean
as $$
string emailRegex = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$";
return EmailAddress.matches(emailRegex);
$$;
```
#### Action with Type Parameter (Generic)
```mdl
/** Validates any entity - checks that required fields are filled. */
create java action Validation.ValidateEntity(
EntityType: entity <pEntity> not null,
InputObject: pEntity not null
) returns boolean
as $$
return InputObject.getMembers().values().stream()
.allMatch(m -> !m.isRequired() || m.getValue(getContext()) != null);
$$;
```
#### Action with Type Parameter + EXPOSED AS
```mdl
/** Deep clones any entity (toolbox-visible). */
create java action Utils.DeepClone(
EntityType: entity <pEntity> not null,
Original: pEntity not null
) returns boolean
exposed as 'Deep Clone Object' in 'Object Utils'
as $$
return true;
$$;
```
## Part 3: Calling Java Actions from MDL
### Basic Syntax
```mdl
-- Call Java action (no return value)
call java action Module.JA_ActionName(
ParamName1 = value1,
ParamName2 = value2
);
-- Call Java action with return value
$Result = call java action Module.JA_ActionName(
ParamName1 = value1,
ParamName2 = value2
);
```
### Avoiding Duplicate Variables (CE0111)
`$Var = call java action ...` **creates a new variable**. Do NOT `declare` a variable with the same name first:
```mdl
-- WRONG: DECLARE + CALL both create $Success → CE0111
declare $success boolean = false;
$success = call java action Module.DoWork();
-- CORRECT: Use a separate name when you need a default
declare $success boolean = false;
$WorkResult = call java action Module.DoWork();
set $success = $WorkResult;
-- CORRECT: Simple pass-through (no default needed)
$success = call java action Module.DoWork();
return $success;
```
When calling Java actions in **multiple branches**, use unique result variable names:
```mdl
declare $success boolean = false;
if $Priority = 'HIGH' then
$UrgentResult = call java action Module.SendUrgent(Msg = $Email);
set $success = $UrgentResult;
else
$NormalResult = call java action Module.SendNormal(Msg = $Email);
set $success = $NormalResult;
end if;
```
### Expression Escaping in String Arguments
Single quotes within string literal arguments must be doubled (`''`):
```mdl
-- OQL with embedded quotes — use '' to escape
$count = call java action Module.ExecuteOQL(
Statement = 'SELECT * FROM Module.Entity WHERE Status = ''Active'''
);
```
### Complete Examples
#### Example 1: Simple Calculation
```mdl
/**
* Calculate tax using custom Java action
*/
create microflow Tax.ACT_CalculateOrderTax($order: Tax.Order)
returns decimal as $taxAmount
begin
declare $subtotal decimal = $order/Subtotal;
declare $taxRate decimal = 0.21;
-- Call Java action for complex calculation
$taxAmount = call java action Tax.JA_CalculateTax(
Amount = $subtotal,
TaxRate = $taxRate
);
change $order (TaxAmount = $taxAmount);
commit $order;
return $taxAmount;
end;
```
#### Example 2: External API Integration
```mdl
/**
* Send notification via external service using Java action
*/
create microflow Notifications.ACT_SendOrderConfirmation($order: Sales.Order)
returns boolean as $success
begin
declare $customerEmail string = $order/Sales.Order_Customer/Email;
declare $orderNumber string = $order/OrderNumber;
-- Call Java action that integrates with external email service
$success = call java action Notifications.JA_SendEmail(
ToAddress = $customerEmail,
Subject = 'Order Confirmation: ' + $orderNumber,
body = 'Your order has been confirmed.',
TemplateName = 'OrderConfirmation'
);
if $success then
change $order (NotificationSent = true);
commit $order;
else
log warning node 'Notifications' 'Failed to send email for order: ' + $orderNumber;
end if;
return $success;
end;
```
#### Example 3: OQL Bulk Operations (Mendix 11.6+)
```mdl
/**
* Bulk update using OQL via Java action
*/
create microflow Finance.ACT_ArchiveOldTransactions()
returns integer as $rowsAffected
begin
-- Use built-in OQL execution Java action
$rowsAffected = call java action CustomActivities.ExecuteOQLStatement(
OqlStatement = 'UPDATE Finance.Transaction SET Status = ''ARCHIVED'' WHERE TransactionDate < ''2024-01-01'' AND Status = ''COMPLETED'''
);
log info node 'Finance' 'Archived ' + toString($rowsAffected) + ' transactions';
return $rowsAffected;
end;
```
#### Example 4: OQL with Parameters
```mdl
/**
* Parameterized OQL update via Java action
*/
create microflow Finance.ACT_UpdateTransactionStatus(
$oldStatus: string,
$newStatus: string,
$cutoffDate: datetime
)
returns integer as $rowsUpdated
begin
$rowsUpdated = call java action CustomActivities.ExecuteOQLStatementPars(
OqlStatement = 'UPDATE Finance.Transaction SET Status = {1} WHERE Status = {2} AND TransactionDate < {3}' with (
{1} = $newStatus,
{2} = $oldStatus,
{3} = $cutoffDate as datetime
)
);
return $rowsUpdated;
end;
```
#### Example 5: Returning Objects
```mdl
/**
* Create complex object structure using Java action
*/
create microflow Import.ACT_ParseCSVFile($fileDocument: System.FileDocument)
returns list of Import.ImportRecord as $records
begin
-- Java action parses CSV and returns list of objects
$records = call java action Import.JA_ParseCSV(
FileDocument = $fileDocument,
HasHeader = true,
Delimiter = ','
);
if $records = empty then
log warning node 'Import' 'No records parsed from file';
else
log info node 'Import' 'Parsed ' + toString(length($records)) + ' records';
end if;
return $records;
end;
```
## Validation Checklist
Before deploying Java actions, verify:
- [ ] Java action has `JA_` prefix naming convention
- [ ] All parameters are defined with correct types
- [ ] Return type matches what you return in Java
- [ ] Code is only between `begin user CODE` / `end user CODE` markers
- [ ] Proper null checks for all parameters
- [ ] Exception handling with logging
- [ ] No hardcoded credentials or sensitive data
- [ ] Entity and attribute names match model exactly
- [ ] Unit tests cover main scenarios
## Common Errors
| Error | Cause | Fix |
|-------|-------|-----|
| `ClassNotFoundException` | Missing library | Add JAR to `userlib/` folder |
| `NullPointerException` | Null parameter | Add null checks |
| `Could not find entity` | Wrong entity name | Use exact qualified name |
| `attribute not found` | Wrong attribute name | Check model for exact name |
| `ClassCastException` | Wrong type cast | Check parameter types |
| `no viable alternative at input '...'` (parse error) | `AS $$ ... $$` body is missing — it is **mandatory** even for void/stub actions | Add `as $$ return null; $$;` (or appropriate stub) |
## Related Documentation
- [Mendix Java Actions Reference](https://docs.mendix.com/refguide/java-actions/)
- [Build Microflow Actions with Java](https://docs.mendix.com/howto/extensibility/howto-connector-kit/)
- [Java Programming in Mendix](https://docs.mendix.com/refguide/java-programming/)
- [Write Microflows Skill](../write-microflows/SKILL.md)
- [Validation Microflows Skill](../validation-microflows/SKILL.md)
## Quick Reference
### Java Action Definition Syntax
```mdl
-- Basic Java action
create java action Module.Name(Param: type not null) returns boolean
as $$
return true;
$$;
-- With type parameters (generics)
-- ENTITY <pEntity> = entity type selector, bare pEntity = entity instances
create java action Module.Name(
EntityType: entity <pEntity> not null,
Obj: pEntity not null
) returns boolean
as $$
return Obj != null;
$$;
-- With EXPOSED AS (toolbox visibility)
create java action Module.Name(Amount: decimal) returns string
exposed as 'Format Amount' in 'Formatting'
as $$
return Amount.toString();
$$;
-- Combined type parameters + EXPOSED AS
create java action Module.Name(
EntityType: entity <pEntity> not null,
Obj: pEntity not null
) returns boolean
exposed as 'Validate Object' in 'Validation'
as $$
return Obj != null;
$$;
```
### Java Action Call Syntax
```mdl
-- Without return value
call java action Module.JA_ActionName(Param1 = value1, Param2 = value2);
-- With return value
$Result = call java action Module.JA_ActionName(Param1 = value1);
-- With OQL parameters (Mendix 11.6+)
$Rows = call java action Module.JA_ExecuteOQL(
Statement = 'UPDATE Module.Entity SET Attr = {1} WHERE Id = {2}' with (
{1} = $value1,
{2} = $value2 as integer
)
);
```
### Core API Quick Reference
```java
// context
IContext context = getContext();
// create
IMendixObject obj = Core.instantiate(context, "Module.Entity");
// read
object value = obj.getValue(context, "attributename");
// update
obj.setValue(context, "attributename", newValue);
// Save
Core.commit(context, obj);
// delete
Core.delete(context, obj);
// query
list<IMendixObject> results = Core.createXPathQuery("//Module.Entity[attr = 'value']").execute(context);
// log
Core.getLogger("ModuleName").info("message");
```