Mendix Workflows Skill
SkillProductivityLets your agent write claude skill files for Mendix workflows with tasks, decisions, timers and parallel branches.
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 Workflows Skill skill
About this capability
Author Mendix workflows in MDL, user tasks, decisions, parallel splits, jumps, waits and boundary events, with CREATE, ALTER and DROP. Use when building a business process with human steps, timers or parallel branches.
What this skill tells your AI
The instructions your AI receives, as published by mendixlabs/mxcli in .claude/skills/mendix/write-workflows/SKILL.md and read by ahel’s review.
Guidance for authoring workflows in Mendix projects with MDL — not just
reading them. CREATE WORKFLOW / DROP WORKFLOW / ALTER WORKFLOW are fully
supported and build in Studio Pro. Workflows are not read-only in mxcli; do
not punt workflow creation to Studio Pro.
When to Use This Skill
- Creating a business process: approvals, reviews, multi-step tasks with user interaction, timers, and parallel branches.
- Adding/removing/reordering activities in an existing workflow (
ALTER WORKFLOW). - Regenerating a workflow from
DESCRIBE WORKFLOWoutput (round-trippable).
A workflow is a Workflows$Workflow unit driven by a context entity: the
persistent entity each workflow instance is about (the Expense being approved,
the LeaveRequest being reviewed). User tasks render a page bound to
System.WorkflowUserTask.
Syntax — CREATE WORKFLOW
The header options may be written in any order — each at most once — and the
body must close with END WORKFLOW. (They used to be order-sensitive, in
exactly the sequence below; a clause written out of place failed with
mismatched input 'DISPLAY' expecting {ON, BEGIN, EXPORT, DUE, OVERVIEW}, which
named neither the clause nor the rule. See ako/mxcli#586.)
create workflow Module.ApprovalFlow
parameter $Context: Module.Request -- REQUIRED: must be a $-variable + context entity
display 'Request Approval' -- optional human-readable name
description 'Approves incoming requests' -- optional
export level Hidden -- optional: Hidden | API (default Hidden)
overview page Module.WF_Overview -- optional; takes a System.Workflow param
on workflow events (UserTaskStarted, UserTaskEnded) -- optional, repeatable
microflow Module.ACT_AuditTask as 'Task audit'
on any workflow event microflow Module.ACT_LogEvent -- every type this Mendix version has
begin
-- activities here, each terminated with ;
end workflow;
Clause order does not matter, but repetition is refused. A workflow's header clauses and a user task's clauses are a set: any order, each at most once. Writing one twice is reported by name —
line 5:2: duplicate DISPLAY clause on workflow Module.ApprovalFlow
(already given on line 4) — each clause may appear at most once, in any order
Three clauses are list-valued and accumulate instead: the header's
on workflow event(s) handlers, and a task's outcomes and boundary event.
The two targeting spellings count as one clause — a user task stores one
user source — so targeting microflow … and targeting xpath … on the same
task is a duplicate, not two clauses. It used to be accepted, with the one
written last silently winning.
Two gotchas that trip up first attempts:
PARAMETERtakes a$-variable then a context entity:parameter $Context: Module.Entity.parameter Module.Entityandparameter name: Module.Entityboth fail (expecting VARIABLE).- The body closer is
end workflow, notend.end;fails (missing WORKFLOW). - The overview page takes a
System.Workflowparameter, not the workflow's context object. Measured on mxbuild 11.6.6: a page without one isCE7410 "The selected page 'Overview' should accept a parameter of type 'Workflow'". (The task page takesSystem.WorkflowUserTaskinstead — two different pages, two different parameters.)
The context is always stored as WorkflowContext. Whatever you name the
variable in the header, mxcli writes the parameter as WorkflowContext, so
$WorkflowContext/Attribute is the canonical way to reach it in an expression.
The name you declared ($Context above) and any casing of the canonical name
($workflowContext) are rewritten to it on write — in decision conditions, user
task due dates and XPath targeting, wait-for-timer delays, and with (…)
parameter mappings. Anything else is an undefined variable and Mendix fails the
build with CE0117 "Error(s) in expression.".
create or replace workflow … and create or modify workflow … are supported.
Activities
Every activity statement ends with ;. Blocks { … } nest a sub-flow.
create or replace workflow Module.ApprovalFlow
parameter $Context: Module.Request
begin
-- User task: renders a page, offers named outcomes (branches)
user task Review 'Review the request'
page Module.ReviewPage
targeting users microflow Module.ACT_Reviewers -- or: targeting users xpath '[Active = true()]'
on created microflow Module.ACT_AssignReviewer -- optional: runs when the task is created
description 'Please review'
outcomes
'Approve' { call microflow Module.ACT_Process; }
'Reject' { call microflow Module.ACT_Notify; };
-- Multi user task: same clauses, one task per targeted user
multi user task GroupSignoff 'Group sign-off'
page Module.ReviewPage
outcomes 'Done' { };
-- Call a microflow (server logic); optional name, parameter mapping + outcomes
call microflow Module.ACT_Validate as callMicroflow1
with (Module.ACT_Validate.Item = '$WorkflowContext');
-- Decision: a boolean or enum exclusive split. The name is optional; give one
-- when a `jump to` targets it.
decision decision1 '$WorkflowContext/Total > 1000'
outcomes
true -> { call microflow Module.ACT_Escalate; }
false -> { call microflow Module.ACT_AutoApprove; };
-- An enum decision: each outcome is a FULLY QUALIFIED enumeration value
-- (Module.Enumeration.Value), plus one '' outcome for "none of the above".
decision decision2 '$WorkflowContext/Status'
outcomes
'Module.ENUM_Status.Approved' -> { }
'Module.ENUM_Status.Rejected' -> { }
'' -> { };
-- Parallel split: independent branches run concurrently
parallel split split1
path 1 { call microflow Module.ACT_Notify; }
path 2 { call microflow Module.ACT_Log; };
-- Wait for a timer, then continue (duration is a Mendix expression)
wait for timer timer1 'addHours([%CurrentDateTime%], 1)';
-- Wait for an external notification (e.g. an event)
wait for notification waitForNotification1;
-- An intermediate notification event (Mendix 11.11+): what `notify workflow`
-- targets by name
notification DocumentsReceived comment 'Documents received';
-- Loop back, or stop the whole workflow, from inside an outcome. A `jump to`
-- and an `end workflow` must each END their path, so neither can close the
-- main flow itself (CE6679 / CE6671).
user task Confirm 'Confirm the booking'
page Module.ReviewPage
outcomes
'Redo' { jump to Review; }
'Cancel' { end workflow comment 'Cancelled'; }
'Done' { };
-- Call a sub-workflow
call workflow Module.SubProcess as callWorkflow1 comment 'delegate';
end workflow;
Do NOT use
annotation '...'in a workflow body. It parses, but the annotation is written into the workflow's activity flow, which Mendix loads by constructing every child with aFlowparent — no annotation type takes one, so the resulting.mprcannot be loaded at all: Studio Pro will not open the project andmx checkfails before validating anything.mxclinow refuses the statement (MDL-WF04) at both check and exec time. Keep the note as an MDL comment (-- ...); workflow canvas annotations are not yet writable.
Boundary events attach a timer to a user task / call-microflow / wait:
create or replace workflow Module.WithBoundary
parameter $Context: Module.Request
begin
user task Review 'Review'
page Module.ReviewPage
outcomes 'Done' { }
boundary event interrupting timer 'addDays([%CurrentDateTime%], 3)' {
call microflow Module.ACT_Escalate;
};
end workflow;
- Name the kind —
interruptingornon interrupting. A bareboundary event timerwrites a type no Mendix 11 runtime has:checkand mxbuild pass, and the runtime then refuses to start the application ("Class 'Workflows$TimerBoundaryEvent' could not be found"). mxcli refuses the bare form on Mendix 11 (MDL-WF07). - The delay is a DateTime expression, such as
'addDays([%CurrentDateTime%], 3)'— not an ISO duration like'P3D'. - Every boundary path must end in a jump, an end, or Mendix's end-of-path
marker, and mxcli now appends the marker for you — so a path may end in a
call microflow, as above. Without it the two kinds fail in different places: an interrupting path is CE0105 at build, and a non-interrupting one builds cleanly and then stops the runtime from starting ("Expected the flow to end with an end event"). Usejump to <task>when the path should return to the task. - A notification boundary event (Mendix 11.11+) fires when
notify workflowtargets it, so it takes a name instead of a delay:boundary event interrupting notification Withdrawn 'Request withdrawn' { end workflow; }. The name is unique in the workflow. Only one interrupting boundary event per activity, of either kind (CE6697, MDL-WF15).alter workflow … insert boundary eventcannot add one yet — restate the workflow. - Over MCP (
--mcp), Studio Pro dictates how a notification path ends, which mxbuild does not: an interrupting one ends inend workflow;(injump toinside a parallel split), a non-interrupting one runs to its end. mxcli refuses the other shapes with that remedy, because Studio Pro's constructor would rewrite or reject them.
Event sub-processes are flows outside the main flow, written after the main body.
A notification (11.8+) or a timer (11.13+) starts one while the workflow runs;
interrupting cancels every active path first, non interrupting runs alongside:
create or modify workflow HR.Leave
parameter $Context: HR.Request
begin
user task Review 'Review' page HR.ReviewPage outcomes 'Approve' { } 'Reject' { };
event subprocess ESP_Cancel 'Cancel request'
on interrupting notification espCancelStart 'Cancel received' {
call microflow HR.ACT_LogCancel;
};
event subprocess ESP_Reminder 'Daily reminder'
on non interrupting timer 'addDays([%CurrentDateTime%], 1)' as espReminderStart {
call microflow HR.ACT_Remind;
};
end workflow;
- The End is implicit, as in the main flow: mxcli appends one unless the body
already ends (
end workflow, ajump to, or branches that all end). A body with no end is CE0105. jump tostays inside its sub-process — a jump to its own activities or its start event builds; into another sub-process, or between one and the main flow, is CE6682 (MDL-WF05).- A timer start needs its expression (CE0126, MDL-WF14).
- Names are shared with the main flow: a start event named like an activity is CE0495, so mxcli makes it unique.
DROP WORKFLOW
drop workflow Module.ApprovalFlow;
ALTER WORKFLOW
In-place edits go through the workflow mutator — no full rewrite. Supports
SET properties, and INSERT / DROP / REPLACE of activities, outcomes,
parallel paths, decision conditions, and boundary events. Reference an activity
by its name (or an auto-named one by its caption in quotes).
Each operation is its own statement — there is no { … } wrapper, and SET
uses no = (set display 'X', not set display = 'X'):
alter workflow Module.ApprovalFlow set display 'Updated Approval';
alter workflow Module.ApprovalFlow set activity Review page Module.AltReviewPage;
alter workflow Module.ApprovalFlow insert after Review call microflow Module.ACT_Log;
alter workflow Module.ApprovalFlow replace activity ACT_Validate with call microflow Module.ACT_Process;
Consecutive sets may chain in one statement:
alter workflow Module.ApprovalFlow set display 'X' set description 'Y';
See mdl-examples/doctype-tests/24-workflow-examples.mdl for the full ALTER
surface (insert path, drop path, insert condition, boundary events).
The INSERT op has to match the activity kind. An activity's outcome list is typed, and each op writes exactly one outcome type into it:
| Op | Writes | Only on |
|---|---|---|
insert outcome '<name>' on X { } | UserTaskOutcome | a user task |
insert condition '<Module.Enum.Value>' on X { } | …ConditionOutcome | a decision, a call microflow |
insert path on X { } | ParallelSplitOutcome | a parallel split |
insert boundary event on X interrupting timer '<expr>' { } | a boundary event | user task, call microflow, call workflow, wait for notification |
Aim one at the wrong kind and the outcome lands in a list that cannot hold it,
which is not a build error: the project stops loading, so Studio Pro will
not open it and mx check dies before it validates anything (ako/mxcli#415).
mxcli refuses all of these now — at check --references and at exec, which
call the same function — and the refusal names the op that fits the target. The
drop ops are unaffected: removing a branch cannot write a wrong type, and it
leaves an ordinary build error (CE6686) rather than an unloadable project.
DESCRIBE round-trip
DESCRIBE WORKFLOW Module.Name emits executable, re-runnable MDL — user
tasks, decisions, splits, jump-to targets, wait activities and boundary events
all come back as statements (not comments). You can learn the exact syntax by
describing a Studio-Pro-authored workflow, and describe → drop → exec
reproduces a workflow that builds. (The implicit start/end activities are
omitted, as they are re-synthesised on create.)
Event sub-processes come back as event subprocess … on … blocks after the main
body, and notification activities and notification boundary events as statements.
Activity names, and why jump to depends on them
Mendix stores JumpToActivity.TargetActivity as an activity name string, not
a pointer — so a jump is only as good as the name it aims at. Every activity type
takes an optional explicit name (as <name> for the two call activities, a bare
name for the rest); without one mxcli derives it from the caption, or from the
called document for call microflow / call workflow.
That default is fine for a workflow written from scratch, and it is why two
decisions sharing a caption used to collide on one name. It is not fine when
reproducing a workflow Studio Pro authored: Studio Pro names activities by type
and ordinal — decision1, split1, callMicroflow1, userTask1,
waitForNotification1 — with no relation to the caption. describe workflow
emits the stored name whenever it is not derivable, so the jump wiring survives a
re-execution; before that it did not, and a jump to decision1 reached MxBuild as
a jump to itself (CE6681, "not possible to jump to end activities or jump-to
activities" — an error naming a different fault). See ako/mxcli#408.
mxcli check resolves every jump against the activity names the script itself
declares (MDL-WF05) and lists the valid targets when one misses.
Rewriting an existing workflow
CREATE OR REPLACE|MODIFY WORKFLOW rebuilds the workflow from the statement,
so anything the script does not restate is deleted — including each boundary
event's whole handler flow. This is the failure that costs real work: it is not
reported by mx check afterwards, because the result is a perfectly valid
workflow that simply no longer does what it did.
mxcli refuses the two cases where that would lose something:
- more stored event sub-processes or notification activities than the statement declares — restate them; a sub-process with no start event, which MDL cannot state, is refused outright;
- more stored boundary events than the statement declares — restate them and
the rewrite proceeds, which is what
describe workflownow emits for you; - more stored workflow event handlers, or user tasks with an on-created microflow, than the statement declares — the same: restate them.
The safe way to change one activity in a workflow carrying hand-placed structure
is ALTER WORKFLOW, which mutates in place and touches nothing else.
Microflow statements for workflow tasks
These run inside a microflow (not in the workflow body) and drive a running
workflow / its tasks. They are easy to miss — there is no complete task:
set task outcome $Task 'Approve';— completes aSystem.WorkflowUserTaskwith a named outcome. This is how a microflow (e.g. a task page's button) finishes a task and does the domain work; the outcome branches still record which one was chosen.$Notified = notify workflow $Wf target Module.Workflow.Name;resumes the element it names — a notification-started event sub-process, a notification activity, a notification boundary event or a wait for notification. The target is required: a notify without one fails the build (CE0166, MDL-WF16). Name the element asModule.Workflow.ElementName; mxcli works out which kind it is and refuses one a notification cannot reach (a timer start, a user task).open user task $Task,lock workflow $Wf, andworkflow operation abort|pause|restart|retry|continue $Wfare also statements.
A common shape: the task page's buttons call a microflow that does the change and
then set task outcome $Task '<Outcome>', leaving the workflow's outcome branch
bodies empty.
Claim the task before completing it
set task outcome on a task nobody has claimed fails at runtime, and it fails
quietly — the button appears to do nothing and the only trace is in the runtime log:
ERROR - Client: You can't complete this user task, it is not assigned to you.
mxcli check and mx check both pass; the build is clean. mxcli check now warns
about it (MDL-WORKFLOW10), but the platform rule is worth knowing rather than
being told.
The trap is that targeting xpath / targeting microflow decides who may SEE a
task — it does not assign it. There is no assign task statement; claiming is a
plain write to the Assignees association, and it must come first:
create microflow Module.ACT_CompleteTask ( $Task: System.WorkflowUserTask )
begin
change $Task (System.WorkflowUserTask_Assignees = [%CurrentUser%]);
commit $Task;
set task outcome $Task 'Plan';
end;
If the task is claimed somewhere else — earlier in the process, or in a microflow this one calls — the warning does not apply.
Related: WorkflowUserTask.Name holds the task's CAPTION, not the activity
name. A task declared user task "ReviewAndPlan" 'Review and plan' stores
Name = 'Review and plan', so routing an inbox on the activity name silently never
matches. Route on your own entity's status instead.
System-module enumerations are synthesized, not stored
The System module's enumerations are not in the project file — Mendix ships
them with the platform — so mxcli synthesizes them from its own table of platform
definitions. describe enumeration System.WorkflowUserTaskState and
show enumerations report them, read-only:
mxcli -p app.mpr describe enumeration System.WorkflowUserTaskState
They used to return nothing, which is why guessing a value and hitting CE1613
"The selected enumeration value no longer exists" was the only way to find out
(mendixlabs/mxcli#1102). Check the values before branching on one — they are
case-sensitive, and WorkflowActivityState (Finished) is a different
enumeration from WorkflowActivityExecutionState (Completed).
Constraining on an attribute ([EndTime = empty] selects open tasks) is still
often the better XPath, but it is no longer a workaround for not knowing the
values. The full list and the System entities are in system-module.
Platform rules
-
Some workflow state has no MDL spelling, and a rewrite refuses rather than reset it. An event sub-process and a workflow event handler subscribed to no event types are set in Studio Pro.
create or modifyon a workflow that holds any of them is refused with the list, and so isalter workflow … replace activityon an activity that holds one. Change such a workflow withalter workflow … set activity …(it edits the stored document and keeps the rest) or in Studio Pro. -
end workflowends the whole workflow from inside a branch — the workflow counterpart of a microflow'sreturn.return;itself is refused in a workflow (MDL-WF11): inside a{ }block it reads as "leave this block", which is exactly the fallthroughend workflowprevents. Measured placement rules (mxbuild 11.13, both engines), all checked without a project:- legal as the last statement of a user-task outcome, a decision branch, a call-microflow outcome or an interrupting boundary-event path, at any depth;
- refused under a parallel split or a non-interrupting boundary-event
path, at any depth —
CE1844,MDL-WF08(a path cannot end the workflow while the others run; jumping out of a path is refused too,CE6682); - refused with anything after it in its block —
CE6671,MDL-WF09; - when every path of an activity ends — in
end workfloworjump to, also through a nested decision — nothing may follow it, not even the end of the main flow:CE6689,MDL-WF10. Let one path continue; a path that reaches the end of the workflow needs noend workflow. - The main flow needs none: the body's closing
end workflowis its End. An outcome left empty does not stop anything — it rejoins the main flow.comment '…'sets the End's caption, as on every workflow activity.
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-workflows- Source
- github.com/mendixlabs/mxcli