Fundamentals 4: repetition
A <repetitionRule> (162 corpus files) makes a plan item able to run more than once. It is the construct with the most surprising interaction in CMMN, and the section at the bottom of this page is the one to read.
How repetition works
Section titled “How repetition works”A plan item with a repetition rule does not disappear when it completes. Instead:
- The instance completes.
- A fresh instance is created in
wait_repetition. wait_repetitionis one of the only two states in which entry criteria are re-evaluated (the other isavailable), so the new instance is watching its sentry again.- When the sentry next fires, the new instance starts.
That is the whole mechanism. Repetition is not a loop construct — it is “put a fresh instance back on the board, waiting for the same guard”.
A rule with no <condition> means always repeat, not “unset”. Orvanta models the rule and its condition as two separate optional things precisely so those two cases stay distinguishable.
repetitionCounter
Section titled “repetitionCounter”Each repeated instance gets a local variable holding its 0-based index. The default name is repetitionCounter — Flowable’s RepetitionRule.DEFAULT_REPETITION_COUNTER_VARIABLE_NAME — and flowable:counterVariable (283 corpus occurrences) renames it.
The variable is local to the instance, so each repetition sees its own value, and it is only created when the plan item actually has a repetition rule.
You can read it in a sentry if-part or in the repetition rule’s own condition:
<itemControl> <repetitionRule flowable:counterVariable="attempt"> <condition><![CDATA[${attempt < 3}]]></condition> </repetitionRule></itemControl>Note the condition must be inside the decidable subset — ${attempt < 3} is a comparison against a literal, so it is fine. Three corpus documents are refused at deploy on an undecidable repetition condition.
maxInstanceCount
Section titled “maxInstanceCount”flowable:maxInstanceCount (50 corpus occurrences) caps the repetition. Orvanta executes both values:
| Value | Meaning |
|---|---|
unlimited (default) | Repeat as long as the condition holds |
one | At most one instance at a time |
Collection-based repetition
Section titled “Collection-based repetition”The second form of <repetitionRule>: flowable:collectionVariable plus flowable:elementVariable and flowable:elementIndexVariable — “run this once per item in that list”. Orvanta executes it.
It behaves differently from the counter-based form in one way worth internalising: the collection is resolved once, at the moment the plan item would activate, and one instance is created per element, all at once. It is not a loop that re-reads the list. Changing the collection variable afterwards changes nothing.
- An empty or absent collection runs no instances and terminates the item.
- A value that is present but not an array is a runtime error, not a guess.
flowable:ignoreCounterVariable="true"suppresses the repetition-counter local.
Which collection shapes are still refused
Section titled “Which collection shapes are still refused”Refusal is a deploy-time error naming the element and the attribute — never a silent approximation.
| Refused | Why not approximate |
|---|---|
A computed collectionVariable | An arbitrary expression producing a value sits outside the decidable subset. A bare ${name} reference is supported: that is a variable read, not a computation. |
A collection combined with a <condition> | Flowable’s precedence between the two is not recorded here, and zero corpus documents combine them. |
A collection combined with a maxInstanceCount other than unlimited | The collection’s length already fixes the instance count; how Flowable reconciles a concurrency bound with it is not recorded here. |
flowable:variableAggregation
Section titled “flowable:variableAggregation”Folding results across repeated instances. Orvanta executes the plain form: a target plus <variable source="…" target="…"/> children fold into an array of one object per instance, written when every instance has reached an end state.
Which aggregations are still refused
Section titled “Which aggregations are still refused”| Refused | Why not approximate |
|---|---|
createOverviewVariable="true" | Flowable writes a second variable whose shape is not transcribed in Orvanta. Writing a differently-shaped one is worse than writing none, because a reader would trust it. |
A class or delegateExpression aggregator | A custom Java aggregator; there is nothing to run. |
targetExpression / sourceExpression | An arbitrary expression producing a value, outside the decidable subset. |
storeAsTransientVariable="true" | This engine has no transient scope, so honouring it would persist a variable the model asked not to persist. |
| Aggregation on a counter-based rule | A counter-based rule has no bounded instance set, so there is no defined moment at which the aggregate is final. |
There is no partial version of any of these: an aggregation that silently produced an incomplete collection would be worse than a refusal, because nothing downstream could tell the difference.
The authority for both sections is the generated backend/orvanta-cmmn/engine-support.json, and a single classifier in the engine crate decides both what deploy admits and what the engine runs — so a shape that is admitted cannot surprise the engine, and a shape the engine cannot run cannot be admitted.
Repetition plus a conditional sentry
Section titled “Repetition plus a conditional sentry”Here is the trap, and it is the reason the trigger modes exist.
Consider a repeating task whose entry criterion has a sentry with:
- an on-part watching some other item’s
complete, and - an if-part like
${moreWorkNeeded}.
Under the default eventDeferred trigger mode, satisfied on-parts latch. So:
- The source item completes. The sentry’s on-part is now satisfied, and it stays satisfied.
- The if-part is true, so the sentry fires and the task starts.
- The task completes; a fresh instance appears in
wait_repetition, evaluating the same criteria. - The on-part is still latched. The moment the if-part is true again — for any reason, at any later point — the sentry fires again, with no new event having occurred.
That is an unintended re-activation, and on a canvas it looks identical to a model that behaves correctly.
The fix
Section titled “The fix”Set the sentry’s trigger mode to onEvent. On-parts then do not latch: every on-part and the if-part must hold within a single evaluation cycle. The sentry can only fire at the instant the connector actually fires, so a repetition cannot re-trigger on a stale event.
The cost is the mirror image, and it is worth stating: under onEvent, if the if-part is false at the moment the event arrives, the sentry never fires at all — it does not remember the event and wait for the condition. That is the correct behaviour for the repetition case and the wrong behaviour for “A now, B much later”.
The rule of thumb: repetition plus a conditional sentry wants onEvent. Almost everything else wants the eventDeferred default.
Non-terminating models
Section titled “Non-terminating models”A CMMN case can genuinely oscillate — two plan items whose exit sentries each fire on the other’s completion, both repeating, is a legal CMMN model and a non-terminating one. Flowable has the same property and no guard against it.
Orvanta’s engine bounds one evaluation to 10,000 cycles and stops with an error rather than looping. The bound is deliberately far above any real model: the deepest document in the 968-file corpus settles in single-digit cycles. If you hit it, the model oscillates.
Fundamentals 5: stage and case completion — when a stage is finished, and the idiom for genuinely optional work.