Mendix Microflow Skill
SkillDev toolsHelps 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.
No other account needed.
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 ofif,case,split type,loopandwhile, plus error handling (try/custom handlers) and the@position/@merge/@anchoractivity annotations.reference/data-operations.md— create, change, commit, delete; list operations and aggregates;retrievefrom database, association and list; XPath navigation.reference/integration.md—rest callandsend rest request, legacy SOAP, calling Java actions (including theemptyargument and microflow-typed parameters), and file downloads.reference/pitfalls.md— read this whenmxcli checkrejects 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
| Scenario | Use |
|---|---|
| Querying the database | Microflow |
| Calling REST services or external actions | Microflow |
| Running Java actions | Microflow |
| File generation or download | Microflow |
| Transactional commits (rollback on error) | Microflow |
| Background scheduled logic | Microflow |
| Client-side form validation before save | Nanoflow |
| UI navigation and page routing | Nanoflow |
| Calling device features (GPS, phone, camera) | Nanoflow |
| Offline data access and local storage | Nanoflow |
| Calling JavaScript actions (NanoflowCommons) | Nanoflow |
| Showing progress indicators / confirmation dialogs | Nanoflow |
Rule of thumb: A nanoflow runs before the server call. A microflow IS the server call.
Key Differences from Nanoflows
| Aspect | Microflow | Nanoflow |
|---|---|---|
| Execution | Server-side | Client-side (browser/mobile) |
| Database access | Full | No direct access |
| Transactions | Supported | Not supported |
| Java actions | Supported | Not supported |
| JavaScript actions | Not supported | Supported |
| SYNCHRONIZE | Not available | Available (offline sync) |
| File downloads | Supported | Not supported |
| Error handling | Full ON ERROR blocks; RAISE ERROR inside a handler only (main flow = MDL084 / CE0710) | Per-action ON ERROR supported; RAISE ERROR / ErrorEvent forbidden |
| Offline | Not available | Available |
| Binary return type | Supported | Not 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 write | What 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 action | Removes that one entry; the microflow entry is untouched |
… drop icon dark | Clears 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
@excludednever un-excludes. It means "the script does not say", not "make this active" — so re-running acreate or modifyyou 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,describeand 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
returnstatement - 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
declarean object (entity) variable.declarebecomes 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 followingset— whether or not you give it an initializer.mxcli checknow 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
declarea list either. Same Create Variable restriction — Studio Pro rejects a list with CE0053/CE0038, andmxcli checkflags 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/longfail CE0117. Integer division ($a div $b) is always a Decimal even for two integers, and so arerandom()and the duration*Betweenfunctions (secondsBetween,minutesBetween,hoursBetween,daysBetween,weeksBetween). Assigning any of them straight to aninteger/longvariable failsmx checkwith CE0117 (mxcli checknow flags it as MDL041). Either declare the targetdecimal, 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 / MDL041The
calendar*Betweenfunctions (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 norandomIntfunction. Userandom()(returns a Decimal in [0,1)) and round to an integer range — e.g. a value in 0..8 isround(random() * 8). Becauserandom()is a Decimal, assigning it (ordiv,secondsBetween(), and the other duration*Betweenfunctions) directly to anintegervariable fails the build with CE0117 — wrap it inround()/floor()/ceil().mxcli checknow flags an unknown expression function likerandomIntas 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 justcheck: 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()andtrunc()(useround/floor/ceil). If exec rejects a function you believe IS a Mendix built-in, build it once and — if mxbuild accepts it — add it tofuncTableinmdl/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
returnstatements - Division uses
divoperator (not/) - All entity/association names are fully qualified
- CALL MICROFLOW parameter names exactly match target signature (use
describe microflowto verify) - Microflow ends with
/separator - Parameters start with
$prefix - Proper closing for control structures (
end if,end loop)
Tips for Success
- Always use fully qualified names:
Module.Entity,Module.Association - Test incrementally: Create simple microflows first, then add complexity
- Check entity definitions: Ensure all attributes exist before referencing
- Use meaningful variable names:
$Customernot$c,$ProductListnot$list - Comment complex logic: Use
--for inline comments - Log important events: Help with debugging and auditing
- Handle empty cases: Check for
= emptybefore using objects - Use WITHOUT EVENTS appropriately: Only when handlers must be skipped (events are on by default)
- Validate before executing: Use
mxcli check script.mdl -p app.mpr --referencesto 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