Control Flow
MDL microflows support conditional branching, loops, and error handling to control the execution path of your logic.
IF / ELSE
Conditional branching executes different activities based on a boolean expression.
Basic IF
IF $Order/TotalAmount > 1000 THEN
CHANGE $Order (DiscountApplied = true);
END IF;
IF / ELSE
IF $Customer/Email != empty THEN
CALL MICROFLOW Sales.SUB_SendEmail (Customer = $Customer);
ELSE
LOG WARNING 'Customer has no email address';
END IF;
ELSIF
For multi-way branching on anything other than an enumeration, chain the conditions
with ELSIF. (To branch on an enumeration, use CASE (Enum Split)
instead — it maps to a Mendix enum split rather than a chain of decisions.)
IF $Order/TotalAmount > 10000 THEN
CHANGE $Order (DiscountPercentage = 15);
ELSIF $Order/TotalAmount > 5000 THEN
CHANGE $Order (DiscountPercentage = 10);
ELSIF $Order/TotalAmount > 1000 THEN
CHANGE $Order (DiscountPercentage = 5);
ELSE
CHANGE $Order (DiscountPercentage = 0);
END IF;
Mendix has no elsif of its own: each ELSIF arm is a decision in the previous
arm’s false branch, exactly as if you had written a nested IF there. DESCRIBE
prints an else branch that holds nothing but one IF as an ELSIF arm.
Because each arm is a decision on the canvas, it takes the layout annotations a
decision takes (@position, @caption, @merge, @anchor, @curve), written
before the ELSIF keyword:
@caption 'Big order?'
IF $Order/TotalAmount > 10000 THEN
CHANGE $Order (DiscountPercentage = 15);
@caption 'Medium order?'
ELSIF $Order/TotalAmount > 5000 THEN
CHANGE $Order (DiscountPercentage = 10);
END IF;
Comparing Enumerations
An enumeration is compared against its qualified value, never a string literal.
A string literal here fails the build with CE0117 “Error(s) in expression” —
mxcli check does not catch it (see
expression type checking),
so the first sign is a failed build.
-- CORRECT
IF $Order/Status = Sales.OrderStatus.Draft THEN ... END IF;
-- WRONG: CE0117 "Error(s) in expression."
IF $Order/Status = 'Draft' THEN ... END IF;
The same applies to putting an enumeration into a string — concatenating it directly is CE0117, so render it first:
-- CORRECT
LOG WARNING 'Unexpected status: ' + getCaption($Order/Status);
-- WRONG: CE0117
LOG WARNING 'Unexpected status: ' + $Order/Status;
The string form is accepted outside comparisons — in a CREATE/CHANGE member
value, an attribute DEFAULT, and an XPath constraint (where enums live as strings at
the database level). The qualified form works everywhere, so prefer it.
Complex Conditions
Conditions support AND, OR, and parentheses:
IF $Amount > 0 AND $Customer != empty THEN
COMMIT $Order;
END IF;
IF ($Status = 'Active' OR $Status = 'Pending') AND $IsValid = true THEN
CALL MICROFLOW Sales.ProcessOrder (Order = $Order);
END IF;
LOOP (FOR EACH)
Iterates over each item in a list:
LOOP $Line IN $OrderLines
BEGIN
CHANGE $Line (
LineTotal = $Line/Quantity * $Line/UnitPrice
);
COMMIT $Line;
END LOOP;
The loop variable ($Line) is automatically declared and takes the entity type of the list.
LOOP with Nested Logic
LOOP $Order IN $PendingOrders
BEGIN
IF $Order/TotalAmount > 0 THEN
CHANGE $Order (Status = 'Confirmed');
COMMIT $Order;
ELSE
DELETE $Order;
END IF;
END LOOP;
BREAK and CONTINUE
Use BREAK to exit a loop early, and CONTINUE to skip to the next iteration:
LOOP $Item IN $Items
BEGIN
IF $Item/IsInvalid = true THEN
CONTINUE;
END IF;
IF $Item/Type = 'StopSignal' THEN
BREAK;
END IF;
CALL MICROFLOW Sales.ProcessItem (Item = $Item);
END LOOP;
WHILE Loop
Executes a block repeatedly as long as a condition remains true:
DECLARE $Counter Integer = 0;
WHILE $Counter < 10
BEGIN
SET $Counter = $Counter + 1;
LOG INFO 'Iteration: ' + toString($Counter);
END WHILE;
Caution: Ensure the condition will eventually become false to avoid infinite loops.
BEGIN and END WHILE are required, as they are for LOOP.
Error Handling
ON ERROR Suffix
Error handling is applied as a suffix to individual activities. There is no TRY...CATCH block in MDL. Instead, you specify error handling behavior on specific activities.
ON ERROR CONTINUE
Ignores the error and continues to the next activity:
COMMIT $Order ON ERROR CONTINUE;
ON ERROR ROLLBACK
Rolls back the current transaction and continues:
DELETE $Order ON ERROR ROLLBACK;
ON ERROR with Handler Block
Executes a custom error handling block when the activity fails:
COMMIT $Order ON ERROR BEGIN
LOG ERROR 'Failed to commit order: ' + $Order/OrderNumber;
ROLLBACK $Order;
END ERROR;
The handler block can contain any activities – logging, rollback, showing validation messages, etc.
The handler is flow, so it is written BEGIN … END ERROR like every other flow block; add WITHOUT ROLLBACK before BEGIN to keep the database changes made so far. The older brace form, ON ERROR { … }, still parses and builds the same handler, with warning MDL-DEPR540; mxcli fmt --upgrade rewrites it.
A handler that does not end in RETURN or RAISE ERROR falls through: after it
runs, the microflow continues with the statement after the activity, through a
merge placed where the two paths meet. DESCRIBE writes the merge’s position as
@merge(x, y) on the activity, the same annotation a decision uses for the merge
that closes it. A handler that rejoins somewhere else names that point with
JOIN <label> and MERGE <label>.
Error Handling Examples
-- Continue despite retrieval failure
RETRIEVE $Config FROM Admin.SystemConfig FIRST ON ERROR CONTINUE;
-- Custom error handler for external call
$Response = CALL MICROFLOW Integration.CallExternalAPI (
Payload = $RequestBody
) ON ERROR BEGIN
LOG ERROR NODE 'Integration' 'External API call failed';
SET $Response = empty;
END ERROR;
-- Rollback on commit failure
COMMIT $Order ON ERROR ROLLBACK;
Note:
ON ERRORis not supported onEXECUTE DATABASE QUERYactivities.
RAISE ERROR (handler-only)
RAISE ERROR builds Mendix’s error event, which re-raises the error currently
being handled. Mendix allows one only where an error is in scope, so it belongs
inside an ON ERROR BEGIN ... END ERROR block:
CALL MICROFLOW Integration.CallExternalAPI (Payload = $Body) ON ERROR BEGIN
LOG ERROR NODE 'Integration' 'API call failed, re-raising';
RAISE ERROR;
END ERROR;
On the main flow it is refused as MDL084, at any nesting depth — inside an
IF, inside a LOOP, or as the whole body. Studio Pro will not draw that shape,
and mxbuild rejects it with CE0710 “The main flow cannot join an error flow or
end in an error event.” The same applies inside a rule, which the same flow builder
produces.
Mendix has no main-flow “throw” activity. To fail deliberately from the normal path, call a Java action that throws:
CREATE JAVA ACTION Module.JA_RaiseTechnicalError (Message: String NOT NULL) RETURNS Boolean AS
$$ throw new com.mendix.systemwideinterfaces.MendixRuntimeException(Message); $$;
CASE (Enum Split)
CASE branches on an enumeration and compiles to a Mendix enum split. It is not a
general-purpose switch: the source is an enum attribute or variable, and the values are
bare enum member names.
CASE $Order/Status
WHEN Draft, Submitted THEN
LOG INFO 'Not shipped yet';
WHEN Approved THEN
LOG INFO 'Ready to ship';
WHEN Shipped, (empty) THEN
LOG INFO 'Nothing to do';
END CASE;
Rules, each of which mxcli check enforces:
| Rule | Why |
|---|---|
Values are bare identifiers — not 'Quoted', not Module.Enum.Value | Parse error otherwise |
One branch per enum value, including (empty) | A missing (empty) is MDL056; mxbuild reports CE0079 for any uncovered value. Required even when the attribute is not null |
No ELSE | MDL008. An enum split is exclusive with one outgoing flow per value; mxbuild reports CE0079 per uncovered value and CE0773 on the else flow |
No AS alias | Parse error: mismatched input 'as' expecting WHEN |
Several values may share a branch (WHEN Draft, Submitted THEN …). CASE works in
nanoflows on the same terms.
SPLIT TYPE (Object Type Split)
SPLIT TYPE branches on an object’s runtime specialization and compiles to a Mendix
object type decision. Branches use the same WHEN … THEN shape as CASE,
so the two splits differ only in what they branch on.
SPLIT TYPE $Animal
WHEN Zoo.Dog THEN
CAST $Dog;
LOG INFO 'woof';
WHEN Zoo.Cat THEN
LOG INFO 'meow';
WHEN Zoo.Animal THEN
LOG INFO 'some other animal';
WHEN (empty) THEN
LOG INFO 'no animal at all';
END SPLIT;
Use CAST inside a branch to bind the specialized variable its body needs.
| Rule | Why |
|---|---|
| A branch for every subtype and the base entity | mxbuild reports CE0090 “The ‘X’ value should be configured for an outgoing flow” for each type with no branch. The base entity is what covers “none of the more specific ones” |
WHEN (empty) THEN is the null-object branch, not a default | It is taken when the variable is empty. It does not cover unnamed types — measured on 11.13.0, one named type plus an empty branch still gives CE0090 for every other type. Branch on the base entity for that |
| The empty branch cannot be omitted | CE0089. mxcli emits the flow unconditionally, so MDL cannot express a split without one |
A non-void microflow needs a RETURN after END SPLIT; | Branches converge on a merge that continues to the end event — otherwise MDL003 and CE0067 |
The pre-#913 spelling — CASE Zoo.Dog for a branch and ELSE for the empty one — still
parses and builds the identical flow, but warns MDL065. It was replaced because CASE
introduced a branch here while introducing the subject in CASE $x WHEN v THEN, and
because ELSE reads as a default branch and never was one.
SPLIT TYPE works in nanoflows on the same terms.
Unsupported Control Flow
The following constructs are not supported in MDL and will cause parse errors:
| Unsupported | Use Instead |
|---|---|
CASE ... WHEN 'String' ... ELSE ... | Bare enum values and a branch per value — see CASE (Enum Split); CASE itself is supported |
TRY ... CATCH ... END TRY | ON ERROR BEGIN ... END ERROR blocks on individual activities |
Complete Example
CREATE MICROFLOW Sales.ACT_ProcessBatch
FOLDER 'Batch'
BEGIN
DECLARE $Orders List of Sales.Order = empty;
DECLARE $SuccessCount Integer = 0;
DECLARE $ErrorCount Integer = 0;
RETRIEVE $Orders FROM Sales.Order
WHERE Status = 'Pending';
LOOP $Order IN $Orders
BEGIN
IF $Order/TotalAmount <= 0 THEN
LOG WARNING 'Skipping order with zero amount: ' + $Order/OrderNumber;
CONTINUE;
END IF;
@caption 'Process order'
CALL MICROFLOW Sales.SUB_ProcessSingleOrder (
Order = $Order
) ON ERROR BEGIN
LOG ERROR 'Failed to process order: ' + $Order/OrderNumber;
SET $ErrorCount = $ErrorCount + 1;
CONTINUE;
END ERROR;
SET $SuccessCount = $SuccessCount + 1;
END LOOP;
LOG INFO 'Batch complete: ' + toString($SuccessCount) + ' processed, '
+ toString($ErrorCount) + ' errors';
END;