write-microflows
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 sy
SKILL.md.claude/skills/mendix/write-microflows/SKILL.md
GitHub<!-- skill-index: generated by scripts/skill-index.py; make check-skill-index -->
<!-- /skill-index -->
Changing a variable: always
Find it fast
sed -n '<from>,<to>p' SKILL.md one section rather than reading the file;
./mxcli syntax <topic> answers how do I write this for a fraction of either.
| lines | section |
|---|---|
| 34-51 | Reference files |
| 52-61 | When to Use This Skill |
| 62-76 | Changing an Existing Microflow |
| 77-95 | When to Use a Microflow vs a Nanoflow |
| 96-112 | Key Differences from Nanoflows |
| 113-267 | Microflow Structure |
| 268-345 | Variable Declarations |
| 346-403 | Operators |
| 404-420 | Logging |
| 421-446 | Special Values |
| 447-493 | Complete Example |
| 494-550 | Calling Microflows |
| 551-578 | Page Navigation |
| 579-595 | Validation Checklist |
| 596-607 | Tips for Success |
| 608-614 | Related Documentation |
| 615-727 | Quick Reference |
Mendix Microflow Skill
Reference files
SKILL.md covers the shape of a microflow and the decisions. The detail lives beside it:
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),execute database query, 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.
Changing an Existing Microflow
Choose the mode by who owns the microflow (choose-edit-mode):
- Created by your MDL scripts, and not edited in Studio Pro since: edit the script
(or fresh
describeoutput) and re-runcreate or modify. - Authored in Studio Pro: prefer
alter microflow X { insert/replace/drop … }(targets fromdescribe microflow X with handles).create or modifyofdescribeoutput patches too: unchanged writes nothing; a statement (even one whose handler returns), guard clause,returnvalue,ifcondition, header clause, parameter (added/retyped; removed only if unused) or stated@position/@startchange is patched in place (a move keeps the node's flows), as is a loop-body activity with new values (an insert needing room past a stated@position). A redrawn@anchor/@curve, other loop-body change, error handler or otherreturnadded/taken away rebuilds under mdl 0 (MDL-V1-REBUILD: IDs renumbered, merges and curves lost) and is refused undermdl 1;. To add, remove or reorder in a loop body,alter … replacethe whole loop (pitfalls). To rebuild deliberately,dropandcreatein ONE script — the grants carry only within it (pitfalls).
When to Use a Microflow vs a Nanoflow
| Scenario | Use |
|---|---|
| Querying the database | Microflow |
Calling REST services or external actions, or sending email (send email, 11.13+) |
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:
mdl 1;
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 list microflows reports it in the Excluded column.
On a statement it disables that one activity instead — see disabled activities.
mdl 1;
@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:
mdl 1;
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 first;- 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.
Changing a variable: always set
set $Counter = $Counter + 1; changes a variable. $x = … without set is an
activity that creates $x — required under mdl 1; (MDL-V1-SET otherwise).
List operations and aggregates are one statement per activity and never nest:
$Open = filter $Orders by Status = M.Status.Open; then $N = count $Open; —
see reference/data-operations.md.
❌ 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
set $Result = $A + $B; -- Addition
set $Result = $A - $B; -- Subtraction
set $Result = $A * $B; -- Multiplication
set $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
set $Result = $A and $B; -- Logical AND
set $Result = $A or $B; -- Logical OR
set $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 with stacktrace; -- ticks "Include latest stack trace"
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 checkflags an unknown function likerandomIntas MDL044, a Decimal assigned to an integer target as MDL041, and a built-in given the wrong number or kind of arguments (formatDecimal($d, 2): the pattern is a String) as E006/E005.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 or nanoflow (log messages included). Not real:currentDeviceType(),[%CurrentDeviceType%](a CE0117checkmisses) 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
mdl 1;
/**
* 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: `first` binds one object (without it $Order is a list — MDL-RETRIEVE03)
retrieve $Order from Shop.Order
where OrderNumber = $OrderNumber first;
-- 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 task queue Module.RefreshQueue (Parallelism: 2)), and the called flow must return nothing: a queued
microflow with a returns clause fails the build with CE7033 (mxcli check:
MDL088), a queued Java action must returns void or it 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
show page Module.EditPage(Product = $Product);
Every call site binds an argument as Param = expression, with no $ on the
parameter name: call microflow, show page, and widget actions
(action: show page Module.Page(Param = $value)) alike. $Param = $value and
Param: $value still parse but are deprecated (MDL-DEPR006/007); mxcli fmt --upgrade rewrites them.
Settings follow in a with list: show page M.P with NumberOfPagesToClose = 1; closes the current page first (an Integer expression; unstated closes none) — as an argument, (NumberOfPagesToClose: 1) is CE1613.
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 (… first), `$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) -- optional: omit it and mxcli lays the flow out; to re-arrange an existing flow run `mxcli layout flows`
@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 flow-scoped and not stored; it lets a second mention point at the
first. Without it, identical text is two notes — mxcli never merges on text.
Geometry is position: (x, y) and size: (w, h); omitted, the note goes 100px
above the activity at 200×50 (60px per extra), and DESCRIBE omits it again.
An @annotation before a statement's @position/@caption/@color/@anchor
is a free note, attached to nothing (where DESCRIBE prints one): put an
activity's own notes after them.
Page Navigation Pattern
show page Module.Page(Param = $value);
close page;
show home page;
Error Handling Pattern
call microflow ... on error continue; -- Ignore error
call microflow ... on error rollback; -- Rollback on error
call microflow ... on error begin log ...; return ...; end error; -- Custom handler
call microflow ... on error without rollback begin ... end error; -- No rollback
The clause goes on whichever activity may fail, not only on calls:
declare $Name String = 'default' on error begin return 'could not initialise'; end error;
set $Name = $Other/Name on error begin return 'lookup failed'; end error;
change $Order (Status = Shipped) on error begin log error 'could not ship'; return; end error;
log info node 'App' 'starting' on error begin return; end error;
show message 'saved' on error begin return; end error;
-- BLOCKING halts the client until dismissed; after `objects`, before `on error`.
show message 'Hello {1}' type Warning with ({1} = $Name) blocking;
validation feedback $Order/Total message 'must be positive' on error begin return; end error;
show page Module.Page on error begin return; end error;
close page on error begin return; end error;
Two limits, both reported rather than silently ignored:
on error continueis rejected by Mendix (CE6035) oncreate,change,commit,log,show page,close page,show messageandvalidation feedback— MDL076. A custom{ handler }is accepted on all of them;continueis fine ondeclare,set,retrieve,deleteandcall microflow. Measured on 11.14.0 — note that create-variable and change-variable acceptcontinuewhile change-object does not.- List/aggregate activities,
create list,add/remove,cast,show home page: nocontinue(MDL076, 11.12.3, #1252);rollbacktakes every form.
In a nanoflow, almost none of them take a clause at all. change, log,
show page, close page, show message and validation feedback are CE6035
there whichever form is written; only declare and set accept one. See
write-nanoflows.
End the handler. A handler body that does not finish with return or throw
merges back into the main flow, so a variable created after the merge point is
out of scope on the error path — CE0108, which Studio Pro reports for the same
model. Ending the handler (as Studio Pro does when you wire it to an end event)
avoids this entirely.