Mendix Workflows Skill

SkillProductivity

Lets 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.

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 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.)

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 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 a Flow parent — no annotation type takes one, so the resulting .mpr cannot be loaded at all: Studio Pro will not open the project and mx check fails before validating anything. mxcli now 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 — 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 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:

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

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:

OpWritesOnly on
insert outcome '<name>' on X { }UserTaskOutcomea user task
insert condition '<Module.Enum.Value>' on X { }…ConditionOutcomea decision, a call microflow
insert path on X { }ParallelSplitOutcomea parallel split
insert boundary event on X interrupting timer '<expr>' { }a boundary eventuser 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 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 $Wf, and workflow operation abort|pause|restart|retry|continue $Wf are 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 modify on a workflow that holds any of them is refused with the list, and so is alter workflow … replace activity on an activity that holds one. Change such a workflow with alter workflow … set activity … (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. 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
Mendix Workflows Skill (write-workflows): Skill · ahel