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.
Installs into .claude/skills of the current project.
Are you the author of Write Workflows?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/mendixlabs-write-workflows)
---
name: write-workflows
description: "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."
---
# Mendix Workflows Skill
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 WORKFLOW` output (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`.)
```sql
mdl 1;
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:**
- `PARAMETER` takes a **`$`-variable then a context entity**: `parameter $Context:
Module.Entity`. `parameter Module.Entity` and `parameter name: Module.Entity`
both fail (`expecting VARIABLE`).
- The body closer is `end workflow`, **not** `end`. `end;` fails (`missing
WORKFLOW`).
- The **overview page takes a `System.Workflow` parameter**, not the workflow's
context object. Measured on mxbuild 11.6.6: a page without one is
`CE7410 "The selected page 'Overview' should accept a parameter of type
'Workflow'"`. (The **task** page takes `System.WorkflowUserTask` instead —
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 modify workflow …` is supported (`create or replace` is its deprecated spelling, MDL-DEPR001).
## Activities
Expressions are bare, as everywhere in MDL: a decision's condition, a timer's
delay, a due date (`decision $WorkflowContext/Total > 1000`, `due date
addDays([%CurrentDateTime%], 3)`). The older string form (`decision '…'`) still
parses and warns `MDL-DEPR080`; `mxcli fmt --upgrade` rewrites it.
Every activity statement ends with `;`. Blocks `{ … }` nest a sub-flow.
```sql
mdl 1;
create or modify 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(Item = $WorkflowContext) as callMicroflow1;
-- 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 caption '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 caption 'Cancelled'; }
'Done' { };
-- Call a sub-workflow
call workflow Module.SubProcess as callWorkflow1 caption 'delegate';
end workflow;
```
**Notes** attach to an activity with `@annotation '…'` on the line before it,
as in a microflow; the workflow's own note is the header clause
`annotation '…'`, and an event sub-process takes `@annotation` before
`event subprocess`. One note per activity; no other `@` annotation is accepted.
```sql
mdl 1;
create workflow Module.Approve
parameter $WorkflowContext: Module.Request
annotation 'Started from the request form'
begin
@annotation 'Escalates after two days'
user task review 'Review' page Module.Review_Task outcomes 'Done' { };
end workflow;
```
> **Do NOT use a standalone `annotation '...';` statement in a workflow body.**
> It parses, but the note is written into the activity flow, which Mendix loads by
> constructing every child with a `Flow` parent — no annotation type takes one, so
> the resulting `.mpr` **cannot be loaded at all**. `mxcli` refuses it (MDL-WF04)
> at check and exec time. Attach the note to an activity with `@annotation`.
**Boundary events** attach a timer to a user task / call-microflow / wait:
```sql
mdl 1;
create or modify 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** — `interrupting` or `non interrupting`. A bare `boundary event
timer` writes a type no Mendix 11 runtime has: `check` and 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"). Use `jump to <task>` when the path should return to the task.
- **A notification boundary event** (Mendix 11.11+) fires when `notify workflow`
targets 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 into X
{ boundary event … } }` cannot 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 in `end workflow;` (in `jump to` inside
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:
```sql
mdl 1;
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`, a `jump to`, or branches that all end). A body with
no end is CE0105.
- **`jump to` stays 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
```sql
mdl 1;
drop workflow Module.ApprovalFlow;
```
## ALTER WORKFLOW
In-place edits go through the workflow mutator — no full rewrite. `alter
workflow` is the generic alter (the same shape as `alter page`): the operations
go in `{ … }`, properties are set with `set ( Key: value )`, and a fragment is
written exactly as in `create workflow`.
```sql
mdl 1;
alter workflow Module.ApprovalFlow {
set (Display: 'Updated Approval', DueDate: addDays([%CurrentDateTime%], 7));
set (Page: Module.AltReviewPage, Description: 'Check the amount') on Review;
set (Targeting: xpath [Active = true()]) on Review;
insert before Review { call microflow Module.ACT_Prepare; }
insert after Review { call microflow Module.ACT_Log; call microflow Module.ACT_Notify; }
replace ACT_Validate with { call microflow Module.ACT_Process; }
drop ObsoleteStep;
};
```
**Addressing an activity.** A target is the activity's **name** (`Review` —
`describe workflow` prints every name) or its **caption** in quotes
(`'Review the request'`); add `@n` to choose one of several matches. A name wins
over a caption that repeats it. An ambiguous target is refused, and the error
lists the matches (`@1 user task Review, @2 decision Review`) — mxcli never
guesses. Every target is resolved before anything changes, so a refused
statement leaves the workflow untouched. The flow's start activity (`start1`,
caption `'Start'`) is addressable too, but nothing goes before it — `insert
before start1` is refused (it would be `CE9526`); use `insert after start1`.
Workflow keys: `Display`, `Description`, `ExportLevel`, `DueDate`,
`OverviewPage`, `Parameter: $WorkflowContext: Module.Entity`. Activity keys
(with `on <activity>`): `Page`, `Description`, `Targeting: microflow M.F` /
`Targeting: xpath [ … ]`, `DueDate`.
**Adding to an activity: `insert into`.** What goes in the braces is the
activity's own clause, as `create workflow` writes it — and it has to match the
activity kind, because an activity's outcome list is typed:
| Fragment | Writes | Only on |
|----------|--------|---------|
| `insert into X { outcomes '<name>' { … } }` | `UserTaskOutcome` | a user task |
| `insert into X { outcomes '<Module.Enum.Value>' -> { … } }` (or `true`, `false`, `default`) | `…ConditionOutcome` | a decision, a call microflow |
| `insert into X { path { … } }` (`path n` must be the next number) | `ParallelSplitOutcome` | a parallel split |
| `insert into X { boundary event 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 — at `check --references` and at `exec`, which call
the same function — and the refusal names the fragment that fits the target.
**Removing a member: `drop X outcome 'Reject'`**, `drop Decision1 outcome true`
(`false`, `default`), `drop Split1 path 2`, `drop X boundary event`. Removing a
branch cannot write a wrong type; it leaves an ordinary build error (`CE6686`)
rather than an unloadable project. `path n` addresses a **parallel split** only
— on a user task it used to delete the n-th outcome (ako/mxcli#791) and is now
refused; drop a user task's outcome by its value. `drop X boundary event` names
no event, so on an activity with several it is refused under `mdl 1` (under
`mdl 0` it drops the first and warns `MDL-V1-BOUNDARYDROP`).
The old per-action statements (`alter workflow M.W set display 'X';`, `set
activity X page …`, `insert outcome 'N' on X { }`, `drop path 'Path 2' on X`)
still parse and warn `MDL-DEPR140`–`149`; `mxcli fmt --upgrade` rewrites them.
See `mdl-examples/doctype-tests/24-workflow-examples.mdl` for the full surface.
## 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.) `describe` prints
`create or modify workflow`, and re-running it on the workflow it came from
changes nothing: the rewrite keeps the stored names of the activities MDL cannot
name (Studio Pro's `start1`, `end1`, …), the empty flow of an outcome that leads
nowhere, and the empty event sub-process list.
That is for learning the syntax and for workflows your scripts own. **To change an
existing Studio Pro workflow, use `alter workflow`**, never drop → exec: that
re-creates the document with new identities and loses anything MDL cannot express
(see [choose-edit-mode](../choose-edit-mode/SKILL.md)).
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 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 workflow` now 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 a `System.WorkflowUserTask` with 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 as `Module.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 $WfDef`, and
`workflow operation abort|pause|restart|retry|continue $Wf` are also statements.
A lock or unlock names its workflow definition (`$WfDef` or `Module.Workflow`);
`pause all` / `unpause all` after it is Studio Pro's "Pause / Unpause instances".
A bare `lock workflow all` is refused (MDL-WF17) — it built as CE1825.
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.
**The outcome is a literal, by design.** Mendix stores
`Microflows$SetTaskOutcomeAction.Outcome` by name — a reference to one outcome of
the user task, resolved when the app is built — so there is no expression slot to
hold a value computed at runtime. `set task outcome $Task $Outcome;` is a parse
error (under every language version) that says so. A **shared** claim-and-complete
microflow, called from every button with the outcome as a parameter, therefore
needs **one branch per outcome**, each with its own literal:
```mdl
mdl 1;
create microflow Approvals.ACT_CompleteTask (
$Task: System.WorkflowUserTask,
$Outcome: String
)
begin
change $Task (System.WorkflowUserTask_Assignees = [%CurrentUser%]);
commit $Task;
if $Outcome = 'Approve' then
set task outcome $Task 'Approve';
else
set task outcome $Task 'Reject';
end if;
end;
```
With more outcomes, chain `elsif` arms, or give each button its own small
microflow that names its outcome — that keeps the outcome checked against the
task when the app is built, which a runtime string never would be.
### 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:
```sql
mdl 1;
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
`list enumerations` report them, read-only:
```bash
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 modify` on a workflow that holds any of them is refused with the
list, and so is `alter workflow … { replace X with { … } }` on an activity that holds
one. Change such a workflow with `alter workflow … { set ( … ) on X; }` (it
edits the stored document and keeps the rest) or in Studio Pro.
- **`end workflow` ends the whole workflow from inside a branch** — the workflow
counterpart of a microflow's `return`. `return;` itself is refused in a workflow
(`MDL-WF11`): inside a `{ }` block it reads as "leave this block", which is
exactly the fallthrough `end workflow` prevents. 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 workflow` or `jump 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 no `end workflow`.
- The main flow needs none: the body's closing `end workflow` is its End.
An outcome left **empty** does not stop anything — it rejoins the main flow.
`caption '…'` sets the End's caption, as on every workflow activity (`comment '…'` is its deprecated alias, MDL-DEPR104).
- **A multi-user task says who must respond and how their outcomes decide**:
`participants all | <n> | <n> percent`, `decide by …` and `await all users`,
in any order (see the clause-order note below). The rules (`decide by`):
`consensus fallback '<outcome>'`, `majority more than half fallback '…'`,
`majority most chosen fallback '…'`, `threshold <n> percent|votes fallback '…'`,
`veto '<outcome>'`, `microflow Module.Decide`. Omitted means all participants,
consensus falling back to the first outcome, and not waiting. Measured on
mxbuild 11.13:
- consensus, majority and threshold **need a fallback** (`CE1866`) and a veto
needs its outcome (`CE1867`); `check` refuses a missing one, and a name
that is not one of the task's outcomes (`MDL-WF13`);
- the decision microflow must **return String** (`CE5012`); its parameters
are free;
- thresholds and participant counts are **not range-checked** by the build
(0, 101 percent, more votes than users all build), so check them yourself.
A rewrite that does not restate a stored rule, participant count or `await all
users` is refused — each omitted clause would reset it.
- **An AI agent task is `call agent microflow`** (Mendix 11.9+) — the call
microflow statement stored as `Workflows$AIAgentTaskActivity`, with the same
argument list, `as`, `comment`, `outcomes` and boundary events. The microflow is
where the agent is invoked. Measured on mxbuild 11.13 against the identical
call microflow, one rule differs: **its microflow must take a parameter**
(`CE1590 "Missing parameter"`), usually the context object passed as
`(Param = $WorkflowContext)`. Return Boolean or an enumeration to
branch on the answer.
- **Handler microflows have fixed signatures** (measured, mxbuild 11.13):
- `on created microflow` takes exactly `System.WorkflowUserTask` and the context
entity, in either order — anything else is `CE6683` — and returns nothing
(`CE5012`).
- A workflow event handler takes exactly `System.WorkflowEvent`,
`System.WorkflowRecord` and `System.WorkflowActivityRecord`, in any order
(`CE6691`).
- **Event type names are not checked by the build** — an invented one builds at
0 errors and never fires. mxcli refuses an unknown name (`MDL-WF12`) and a type
the project's version does not have. The list is in `mxcli syntax
workflow.event-handlers`.
- `on any workflow event` stores the full list for the project's version (Studio
Pro stores a list, not a flag), so it needs Mendix 11.6+; name the types on
older projects.
- A user task needs a **task page** to be useful; without one Mendix flags the
task (`CE1834`). Bind the page to `System.WorkflowUserTask`.
- **The task page takes the TASK, not the workflow's context object.** It must
declare a `System.WorkflowUserTask` parameter: a page with no parameters is
`CE7410`, a page whose parameters are all something else (the usual mistake:
the context entity) is `CE7412`. Other parameters may sit alongside the task
one — that builds clean. Multi-user tasks follow the same rule.
- **A targeting microflow takes exactly two parameters: `System.Workflow` and
the workflow's context entity**, in either order. One parameter, none, or a
third is `CE6677`. The context parameter may be typed to a *generalization* of
the context entity, not a specialization. `targeting groups microflow` takes
the same two and returns a list of `System.WorkflowGroup`; users targeting
returns a list of `System.User`.
- `mxcli check --references` reports both signatures **before anything is
written**, for pages and microflows in the project or created earlier in the
same script (measured on Mendix 11.13; not applied to older projects). `exec`
refuses the workflow statement itself, so the workflow is never written — but
the statements before it in the script already are. Run `check --references`
first. Plain `mxcli check` without a project cannot see these.
- A user task / decision with a single outcome and no activity can trip
`CE1876` — give each branch a body or a distinct outcome.
- **An enum decision's outcome must be `Module.Enumeration.Value`.** Mendix
stores it as an `EnumerationValueIdentifier` and parses it when the project is
**loaded**, before any consistency check — so a short name is not a build
error with a CE number, it leaves a project Studio Pro and mxbuild cannot open
(`StorageLoadException`). Measured: `'Approved'` and `'Status.Approved'` both
make the project unloadable; `'Sales.ENUM_Status.Approved'` checks at 0
errors. Shortening it because the enumeration is in the same module does not
work. `mxcli check` refuses all three of these as `MDL-WF03`, and `exec`
refuses to run a script it flags.
- **An enum decision also needs one `'' -> { }` outcome** for "none of the
above": Mendix generates one outcome per enumeration value plus the empty one,
and MxBuild compares the stored set against that, so anything else is `CE6686`
("Regenerate the outcomes"). `check` reports a missing one as `MDL-WF06`. It
applies equally to a `call microflow` activity branching on an enumeration
return, and to a decision introduced by `ALTER WORKFLOW … INSERT AFTER` /
`REPLACE ACTIVITY`. A **required (`not null`) attribute does not exempt it** —
measured, the empty outcome is still required. Boolean (`true`/`false`)
decisions do not take one.
- **Arguments go right after the callee, as bare expressions**, like every other
call: `call microflow HR.Escalate(Request = $WorkflowContext) as callMicroflow1`.
The older `with (Request = '$WorkflowContext')`, the expression inside a
string, still parses with the same meaning but is deprecated (MDL-DEPR008);
`mxcli fmt --upgrade` rewrites it.
- The context **Parameter entity must be persistent**.
- Write the context variable as **`$WorkflowContext`**, matching the parameter
name exactly. Mendix expressions are case-sensitive on 11.9+, so a lowercase
`$workflowContext` is an undefined variable and yields `CE0117`.
## Observing a running workflow
A workflow's characteristic failures are **runtime** failures — an instance that
starts and stops, a task that never reaches an inbox, a task page that renders
blank. None of them is visible to `mxcli check`, `mxcli lint` or `mx check`,
which all validate the model rather than the data the model no longer matches.
So do not stop at "it builds".
Everything needed is already a skill — read the one you need rather than
hand-rolling admin-API calls:
| To see | Read |
|---|---|
| Live instances and open tasks (OQL against the running app) | [`verify-with-oql`](../verify-with-oql/SKILL.md), [`write-oql-queries`](../write-oql-queries/SKILL.md) |
| The exception that stopped an instance | [`analyze-runtime`](../analyze-runtime/SKILL.md) — `run --local` tees the runtime log to `<projectDir>/.mxcli/runtime.log` |
| `System.Workflow` / `System.WorkflowUserTask` / `System.WorkflowDefinition` shapes | [`system-module`](../system-module/SKILL.md) |
| Driving a task end to end and asserting the result | [`test-app`](../test-app/SKILL.md), [`run-local`](../run-local/SKILL.md) |
| Raw admin API, incl. `POST /dev/preview_execute_oql` | [`runtime-admin-api`](../runtime-admin-api/SKILL.md) |
Two traps worth knowing before you start:
- **The declared return type is not what the runtime checks.** A workflow-called
microflow whose end event returns a value while the microflow declares no
return type fails at instance start with `Trying to compare
VoidConditionValue$('') to BooleanValue('true')`. `mxcli check` catches this as
**MDL004** — so do not skip it, and do not reach for `--no-check` to get past
it. Read the message in the order it is written: the receiver is the stored
outcome's condition, the argument is what the microflow actually returned.
- **A parked instance is not a failed one.** A wait or timer branch is supposed
to sit there. Check the branch before calling it a hang.
## Validate before presenting
```bash
./bin/mxcli check script.mdl # syntax + activity grammar
./bin/mxcli check script.mdl -p app.mpr --references # entity/page/microflow refs exist
```
Then `list workflows` (lists the workflow, its parameter entity, and activity
count) and, if Docker is available, `mxcli docker build -p app.mpr` for the full
Studio-Pro validation.