Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 ERROR is not supported on EXECUTE DATABASE QUERY activities.

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:

RuleWhy
Values are bare identifiers — not 'Quoted', not Module.Enum.ValueParse 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 ELSEMDL008. 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 aliasParse 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.

RuleWhy
A branch for every subtype and the base entitymxbuild 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 defaultIt 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 omittedCE0089. 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:

UnsupportedUse Instead
CASE ... WHEN 'String' ... ELSE ...Bare enum values and a branch per value — see CASE (Enum Split); CASE itself is supported
TRY ... CATCH ... END TRYON 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;