Mendix Microflow Skill

SkillDev tools

Helps your agent write a Claude skill covering Mendix microflow syntax, activities, control flow, and common errors.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Mendix Microflow Skill skill

About this capability

Microflow syntax reference in MDL, every activity type, control flow, expressions, and the mistakes that fail `mxcli check`. Use before writing any CREATE MICROFLOW, and when debugging a microflow syntax error.

What this skill tells your AI

The instructions your AI receives, as published by mendixlabs/mxcli in .claude/skills/mendix/write-microflows/SKILL.md and read by ahel’s review.

This skill provides comprehensive guidance for writing Mendix microflows in MDL (Mendix Definition Language) syntax.

Reference files

SKILL.md covers the shape of a microflow and the decisions. The detail lives beside it, and is worth opening when you hit one of these:

  • reference/control-flow.md — every form of if, case, split type, loop and while, plus error handling (try/custom handlers) and the @position / @merge / @anchor activity annotations.
  • reference/data-operations.md — create, change, commit, delete; list operations and aggregates; retrieve from database, association and list; XPath navigation.
  • reference/integration.md — rest call and send rest request, legacy SOAP, calling Java actions (including the empty argument and microflow-typed parameters), and file downloads.
  • reference/pitfalls.md — read this when mxcli check rejects something you believe is correct. The anti-patterns, the CE0111 duplicate-variable trap, the syntax that looks plausible and does not parse, and the Studio Pro errors each one produces.

When to Use This Skill

Use this skill when:

  • Writing CREATE MICROFLOW statements
  • Debugging microflow syntax errors
  • Converting Studio Pro microflows to MDL
  • Understanding microflow control flow and structure

If you're not sure whether the logic belongs in a microflow or a nanoflow, read the next section first. The mirror lives in write-nanoflows — keep both copies in sync.

When to Use a Microflow vs a Nanoflow

ScenarioUse
Querying the databaseMicroflow
Calling REST services or external actionsMicroflow
Running Java actionsMicroflow
File generation or downloadMicroflow
Transactional commits (rollback on error)Microflow
Background scheduled logicMicroflow
Client-side form validation before saveNanoflow
UI navigation and page routingNanoflow
Calling device features (GPS, phone, camera)Nanoflow
Offline data access and local storageNanoflow
Calling JavaScript actions (NanoflowCommons)Nanoflow
Showing progress indicators / confirmation dialogsNanoflow

Rule of thumb: A nanoflow runs before the server call. A microflow IS the server call.

Key Differences from Nanoflows

AspectMicroflowNanoflow
ExecutionServer-sideClient-side (browser/mobile)
Database accessFullNo direct access
TransactionsSupportedNot supported
Java actionsSupportedNot supported
JavaScript actionsNot supportedSupported
SYNCHRONIZENot availableAvailable (offline sync)
File downloadsSupportedNot supported
Error handlingFull ON ERROR blocks; RAISE ERROR inside a handler only (main flow = MDL084 / CE0710)Per-action ON ERROR supported; RAISE ERROR / ErrorEvent forbidden
OfflineNot availableAvailable
Binary return typeSupportedNot supported

For nanoflow-specific authoring guidance, see write-nanoflows.

Microflow Structure

CRITICAL: All microflows MUST have JavaDoc-style documentation

/**
 * Microflow description explaining what it does
 *
 * Detailed explanation of the business logic, use cases,
 * and any important implementation notes.
 *
 * @param $Parameter1 Description of first parameter
 * @param $Parameter2 Description of second parameter
 * @returns Description of return value
 * @since 1.0.0
 * @author Team Name
 */
create microflow Module.MicroflowName (
  $Parameter1: type,
  $Parameter2: type
)
returns ReturnType as $ReturnVariable
[folder 'FolderPath']
begin
  -- Microflow logic here
  return $ReturnVariable;
end;

exposed as … action — putting a microflow in the toolbox

Studio Pro can show a microflow in its toolbox, so whoever drags it onto a flow does not need to know it is a microflow rather than a Java action. A microflow has two such entries — one for the microflow editor, one for the workflow editor — so the clause names which:

create microflow Module.FormatCode ($Raw: String)
returns String
exposed as microflow action 'Format code' in 'Toolbox demo'
  icon 'assets/format-64.png'
  icon dark 'assets/format-64-dark.png'
  image 'assets/format-256.png'
exposed as workflow action 'Format code' in 'Toolbox demo'
begin
  return trim($Raw);
end;

The icon should be a 64x64 PNG and the image 256x192; paths resolve against the directory of the .mdl file, so a script and its artwork travel together. A different size is written with a warning; a file that is not a PNG is refused.

An omitted clause preserves what is stored — it does not clear it:

You writeWhat happens
exposed as microflow action 'X' in 'Y'Caption and category from MDL; icon and image carried from what was stored
(no clause)Both entries preserved, bitmaps included
not exposed as workflow actionRemoves that one entry; the microflow entry is untouched
… drop icon darkClears one bitmap; the other three are untouched

Rewriting a microflow's body must not cost it the icon a designer set in Studio Pro, and saying nothing about the toolbox is not asking to be taken out of it. Nothing below Studio Pro sees the difference — mx check reports 0 errors either way — so the preserve rule is the only thing protecting it.

Nanoflows and rules cannot be exposed. Only Microflows$Microflow stores a toolbox entry; the clause is refused on the other two with a message naming the alternative. Java and JavaScript actions have one entry each and use the shorter exposed as 'Caption' in 'Category' — see the java-actions skill.

@excluded — documents excluded from the project

@excluded before a create microflow marks the document "Exclude from project" (the same checkbox Studio Pro offers). The document stays in the .mpr, does not build, and show microflows reports it in the Excluded column.

@excluded
create microflow MyModule.LegacyCalc ()
returns Integer
begin
  return 7;
end;

The sibling document annotation is @applyentityaccess — runs the flow under the current user's entity access rules rather than with full access, with the same absent-preserves rule and an explicit (false) to turn it off (pitfalls).

Two rules follow, and both are enforced rather than documented-and-hoped:

  • An absent @excluded never un-excludes. It means "the script does not say", not "make this active" — so re-running a create or modify you wrote before the document was excluded leaves the exclusion alone. Un-exclude in Studio Pro. (Before #914 the rewrite cleared it, which is how a valid project ended up failing CE0122 — see the next rule.)
  • A name is not unique when a twin is excluded. Mendix allows two documents of the same name in one module as long as at most one is active — verified on 11.13.0: the excluded pair builds at 0 errors, the same pair both active is [error] CE0122 "Duplicate document name". create or modify, describe and the other by-name lookups therefore target the live document; the excluded twin is neither rewritten nor deleted.

The same applies to every document type that carries the flag — nanoflows, pages, snippets, enumerations, queues, workflows, Java/JavaScript actions, mappings, JSON structures, REST/OData services, image collections and the agent documents.

FOLDER Option

Place microflows in folders for organization:

create microflow MyModule.ACT_ProcessOrder ($Order: MyModule.Order)
returns boolean as $success
folder 'Orders/Processing'
begin
  -- logic
  return true;
end;

Key Rules:

  • Parameters start with $ prefix
  • Return variable must be declared or used
  • Every microflow must end with return statement
  • Every body statement ends with a semicolon ; — required, not optional. This includes block terminators: end if;, end loop;, end while;, end case;. A missing one is a parse error (missing ';' at 'return'), not a warning.
  • Microflow ends with / separator

Parameter Types

-- Primitive types
$Name: string
$count: integer
$Amount: decimal
$IsActive: boolean
$date: datetime

-- Entity types
$Customer: Module.Entity

-- List types
$ProductList: list of Module.Product

-- Enumeration types
$status: enum Module.OrderStatus

Variable Declarations

✅ CORRECT Syntax

-- Primitive types with initialization
declare $Counter integer = 0;
declare $message string = 'Hello';
declare $IsValid boolean = true;
declare $Today datetime = [%CurrentDateTime%];
declare $status Enumeration(Module.OrderStatus) = Module.OrderStatus.Open;

You cannot declare an object (entity) variable. declare becomes a Create Variable activity, which Mendix only allows to hold primitive types (String, Integer/Long, Decimal, Boolean, DateTime, Enumeration). An object type is rejected by Studio Pro/mxbuild with CE0053 ("Selected type is not allowed"), plus CE0038 ("Value required") and CE7247 on any following set — whether or not you give it an initializer. mxcli check now flags it as MDL043. There is no "empty object variable" activity. Get objects from one of these:

  • a microflow parameter: create microflow M.Save ($Product: Test.Product) ...
  • a retrieve: retrieve $Product from Test.Product where Code = $c limit 1;
  • a create object: $Product = create Test.Product (Name = $n);
  • a loop iterator: loop $Product in $Products ...

You cannot declare a list either. Same Create Variable restriction — Studio Pro rejects a list with CE0053/CE0038, and mxcli check flags it as MDL040. Get lists from:

  • a microflow parameter: create microflow M.Process ($Items: list of Test.Product) ...
  • a retrieve: retrieve $Products from Test.Product where IsActive = true;
  • a create list: $Products = create list of Test.Product;

Decimal values into an integer/long fail CE0117. Integer division ($a div $b) is always a Decimal even for two integers, and so are random() and the duration *Between functions (secondsBetween, minutesBetween, hoursBetween, daysBetween, weeksBetween). Assigning any of them straight to an integer/long variable fails mx check with CE0117 (mxcli check now flags it as MDL041). Either declare the target decimal, or round it:

declare $Avg decimal = $Total div $Count;          -- ✅ Decimal target
declare $Whole integer = round($Total div $Count); -- ✅ rounded to Integer
declare $Secs integer = round(secondsBetween($a,$b)); -- ✅ rounded to Integer
declare $Bad integer = $Total div $Count;          -- ❌ CE0117 / MDL041

The calendar*Between functions (calendarMonthsBetween, calendarYearsBetween) return whole units (Integer) and are fine to assign directly.

❌ INCORRECT Syntax

-- WRONG: Declaring an object/entity variable (CE0053/CE0038, MDL043)
declare $Product Test.Product;            -- bare object declare is invalid
declare $Product Test.Product = $someObj; -- initialized object declare is also invalid

-- WRONG: Declaring a list variable (CE0053/CE0038, MDL040)
declare $ProductList list of Test.Product = empty;  -- use a parameter, retrieve, or create list

-- WRONG: Using AS keyword (not supported in mxcli)
declare $Product as Test.Product;  -- ERROR: parse error

-- WRONG: No value (CE0038, MDL061)
declare $X string;  -- a Create Variable activity requires a value

-- WRONG: Missing type
declare $Counter = 0;  -- Type inference not always supported

-- WRONG: Using 'OF' instead of 'of'
declare $list list of Test.Product;  -- Case sensitive

Operators

Arithmetic

$Result = $A + $B;      -- Addition
$Result = $A - $B;      -- Subtraction
$Result = $A * $B;      -- Multiplication
$Result = $A div $B;    -- Division (use 'div', not '/')

Important: Use div for division, NOT /. In a Mendix expression / is the member/association separator ($obj/Attr), so $A / $B is not division — mxcli check rejects it as MDL045 (it would fail the build with CE0117). Integer/decimal division always yields a Decimal; wrap it in round()/trunc() for an Integer result (else MDL041).

Comparison

$A = $B       -- Equals
$A != $B      -- Not equals
$A > $B       -- Greater than
$A >= $B      -- Greater than or equal
$A < $B       -- Less than
$A <= $B      -- Less than or equal
$A = empty    -- Check if empty/null
$A != empty   -- Check if not empty

Boolean Logic

$Result = $A and $B;    -- Logical AND
$Result = $A or $B;     -- Logical OR
$Result = not $A;       -- Logical NOT

-- Complex expressions
if $IsActive and $IsValid and $HasStock then
  set $CanProcess = true;
end if;

Date construction

dateTime(...) / dateTimeUTC(...) build a date from literal numeric constants only — a variable or computed argument fails the build with CE0117 (mxcli check flags it as MDL046). To build a date from variables, step off a literal anchor with addDays() / addMonths() (which do take variables):

-- WRONG: variable args to dateTime() (CE0117 / MDL046)
set $D = dateTime(2026, $Month, $Day);

-- RIGHT: anchor on a literal, then step with addMonths/addDays
set $D = addDays(addMonths(dateTime(2026, 1, 1), $Month - 1), $Day - 1);

Logging

-- Log levels
log info 'Information message';
log warning 'Warning message';
log error 'Error message';

-- With node name
log info node 'OrderService' 'Processing order';
log warning node 'ValidationService' 'Invalid data detected';

-- With variables (use concatenation)
log info node 'OrderService' 'Order processed: ' + $OrderNumber;
log error node 'Service' 'Error: ' + $ErrorMessage;

Special Values

empty                      -- Null/empty value
[%CurrentDateTime%]        -- Current date/time
[%CurrentUser%]            -- Current user object
toString($value)           -- Convert to string

No randomInt. Mendix has no randomInt function. Use random() (returns a Decimal in [0,1)) and round to an integer range — e.g. a value in 0..8 is round(random() * 8). Because random() is a Decimal, assigning it (or div, secondsBetween(), and the other duration *Between functions) directly to an integer variable fails the build with CE0117 — wrap it in round()/floor()/ceil(). mxcli check now flags an unknown expression function like randomInt as MDL044 (with a "did you mean random()?" hint), and a Decimal assigned to an integer target as MDL041 — before the build does.

MDL044 also blocks mxcli exec, not just check: a call to a name Mendix has no built-in for is CE0117 at build time, so exec refuses to write the microflow rather than leaving you to find out from mxbuild. Two names that look plausible and are not real: currentDeviceType() and trunc() (use round/floor/ceil). If exec rejects a function you believe IS a Mendix built-in, build it once and — if mxbuild accepts it — add it to funcTable in mdl/exprcheck/func_checker.go; that table is the rule's only allow-list.

Complete Example

/**
 * Process order with validation and status update.
 *
 * @param $OrderNumber The order to process
 * @returns true when the order was found and marked processed
 */
create microflow Shop.ProcessOrder (
  $OrderNumber: string
)
returns boolean as $success
begin
  declare $success boolean = false;

  -- Find the order (the retrieve establishes $Order — objects are never declared)
  retrieve $Order from Shop.Order
    where OrderNumber = $OrderNumber;

  -- Validate order exists
  if $Order = empty then
    log warning node 'OrderService' 'Order not found: ' + $OrderNumber;
    return false;
  end if;

  -- Validate customer association
  if $Order/Shop.Order_Customer = empty then
    log error node 'OrderService' 'Order has no customer';
    return false;
  end if;

  -- Update order status
  change $Order (
    status = 'PROCESSING',
    ProcessedDate = [%CurrentDateTime%]);

  commit $Order;

  -- Log success
  log info node 'OrderService' 'Order processed: ' + $OrderNumber;
  set $success = true;
  return $success;
end;
/

Calling Microflows

✅ CORRECT Syntax

-- Call with result assignment (no SET keyword)
$Result = call microflow Module.ProcessOrder(Order = $Order);

-- Call without result (void microflow)
call microflow Module.SendNotification(message = $message);

-- Call with error handling
$Result = call microflow Module.ExternalService(data = $data) on error continue;

-- Run the call on a task queue (background execution). The clause goes after
-- the arguments and before any ON ERROR, and works on CALL JAVA ACTION too.
call microflow Module.ACT_Refresh() in queue Module.RefreshQueue;
call java action Module.RefreshData(Url = $Url) in queue Module.RefreshQueue;

Queued calls — the queue must already exist (create queue Module.RefreshQueue (Parallelism: 2)), and a queued Java action must returns void or the build fails with CE7038. Rewriting a microflow that has a queued call must restate the in queue clause; a rewrite that omits it is refused rather than silently dropping the binding. See .claude/skills/mendix/scheduled-events-and-queues.

❌ INCORRECT Syntax

-- WRONG: Do NOT use SET with CALL MICROFLOW
set $Result = call microflow Module.ProcessOrder(Order = $Order);  -- ERROR!

-- CORRECT: Direct variable assignment
$Result = call microflow Module.ProcessOrder(Order = $Order);

Important: The set keyword is for changing existing variable values, NOT for capturing microflow return values. Use direct assignment ($var = call microflow ...).

Parameter Name Matching

CRITICAL: Parameter names in call microflow must exactly match the parameter names declared in the target microflow's signature (without the $ prefix). A mismatch causes a build error (MxBuild) but may fail silently at MDL execution time.

-- Target microflow declaration:
create microflow Module.SendEmail ($Recipient: string, $Subject: string)
begin ... end;

-- CORRECT: parameter names match the declaration
call microflow Module.SendEmail(Recipient = $Email, Subject = $title);

-- WRONG: parameter name does not match (EmailAddress vs Recipient)
call microflow Module.SendEmail(EmailAddress = $Email, Subject = $title);  -- BUILD ERROR!

When calling microflows, always check the target's parameter list. Use describe microflow Module.Name to see the exact parameter names.

Page Navigation

SHOW PAGE

-- Open page with parameter (canonical syntax)
show page Module.EditPage($Product = $Product);

-- Widget-style syntax also accepted in microflows
show page Module.EditPage(Product: $Product);

Both ($Param = $value) and (Param: $value) syntaxes are accepted in microflow SHOW PAGE statements. Similarly, widget Action: properties accept both show_page Module.Page(Param: $value) and show_page Module.Page($Param = $value).

CLOSE PAGE

close page;

SHOW HOME PAGE

show home page;

Validation Checklist

Before executing a microflow script, verify:

  • No object (entity) or list is declared — objects come from a parameter, retrieve, create object, or loop iterator (MDL043); lists from a parameter, retrieve, or create list (MDL040)
  • All primitive variables are declared before SET (declare $var type = value;)
  • XPath association navigation uses qualified names (Module.AssociationName)
  • All referenced attributes exist in entity definitions
  • Every flow path ends with return
  • No code appears after return statements
  • Division uses div operator (not /)
  • All entity/association names are fully qualified
  • CALL MICROFLOW parameter names exactly match target signature (use describe microflow to verify)
  • Microflow ends with / separator
  • Parameters start with $ prefix
  • Proper closing for control structures (end if, end loop)

Tips for Success

  1. Always use fully qualified names: Module.Entity, Module.Association
  2. Test incrementally: Create simple microflows first, then add complexity
  3. Check entity definitions: Ensure all attributes exist before referencing
  4. Use meaningful variable names: $Customer not $c, $ProductList not $list
  5. Comment complex logic: Use -- for inline comments
  6. Log important events: Help with debugging and auditing
  7. Handle empty cases: Check for = empty before using objects
  8. Use WITHOUT EVENTS appropriately: Only when handlers must be skipped (events are on by default)
  9. Validate before executing: Use mxcli check script.mdl -p app.mpr --references to catch errors

Related Documentation

Quick Reference

Variable Declaration Pattern

declare $primitive type = value;              -- Primitives (String/Integer/Decimal/Boolean/DateTime)
declare $status Enumeration(Module.Enum) = …; -- Enumerations are primitives too
-- Objects: never declare. Use a parameter, retrieve (limit 1), `$obj = create Module.Entity(...)`, or a loop iterator.
-- Lists:   never declare. Use a parameter, retrieve, or `$list = create list of Module.Entity;`

Object Operation Pattern

$var = create Module.Entity (attr = value);
change $var (attr = value);
commit $var [without events] [refresh];

Flow Control Pattern

if condition then ... [else ...] end if;
loop $var in $list begin ... end loop;
return $value;

XPath Pattern

$var/attributename                      -- Attribute
$var/Module.AssociationName             -- Association
$var/Module.AssociationName/attribute   -- Chained

Annotation Pattern

@position(200, 200)
@caption 'Persist order'
@color Green
@annotation 'Note about the next activity'
commit $Order;                                          -- Annotations apply here

Annotations Are Notes, and a Note Can Be Shared

A note is a node with edges in Mendix, not a property of the activity it documents. So @annotation is repeatable — one activity can carry several, each its own note — and one note can be attached to several activities:

@annotation(id: n1, text: 'both of these touch the same record')
commit $Order;
@annotation(id: n1)                     -- attaches THAT note, does not copy it
commit $Invoice;

id: is scoped to the flow you are writing and is not stored in the model; it exists only so a second mention can point at the first. Without it, two lines with identical text are two separate notes — mxcli never merges on text.

A note's own canvas geometry is position: (x, y) and size: (w, h), e.g. @annotation(text: 'note', position: (175, -40), size: (260, 70)). Omit them and the note goes 100px above the activity at 200×50, stacking 60px per extra note; DESCRIBE omits them again whenever they match, so an ordinary note keeps the short @annotation 'text' form.

Execute Database Query Pattern

-- Static query (3-part name: Module.Connection.Query)
$Results = execute database query Module.Conn.QueryName;

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
122
Forks
49
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
write-microflows
Source
github.com/mendixlabs/mxcli