Skip to content

Fundamentals 3: sentries

A sentry is CMMN’s only control-flow mechanism. If one page in this section prevents a real bug, it is this one — the two trigger modes in particular behave differently in a way that is invisible on a canvas.

A criterion is what you attach to a plan item; a sentry is the logic behind it. They are separate elements joined by @sentryRef:

<planItem id="pi_ship" definitionRef="def_ship">
<entryCriterion id="ec_ship" sentryRef="s_paid_and_stocked" />
</planItem>
<sentry id="s_paid_and_stocked">
<planItemOnPart id="op_1" sourceRef="pi_payment">
<standardEvent>complete</standardEvent>
</planItemOnPart>
<planItemOnPart id="op_2" sourceRef="pi_stock_check">
<standardEvent>complete</standardEvent>
</planItemOnPart>
<ifPart>
<condition><![CDATA[${order.total > 0}]]></condition>
</ifPart>
</sentry>

Two kinds of criterion:

  • Entry criterion (<entryCriterion>, 563 corpus files) — the plan item becomes available/active when the sentry is satisfied.
  • Exit criterion (<exitCriterion>, 148 corpus files) — the plan item stops when the sentry is satisfied. On a stage it takes the stage’s children with it; on the case plan model it terminates the whole case.

Orvanta reads @sentryRef in both the spec spelling and flowable:sentryRef (90 corpus occurrences), which works only because attribute lookup is namespace-aware throughout.

An on-part (<planItemOnPart>, 1,003 corpus occurrences) says watch this plan item for this lifecycle event. It carries @sourceRef — the id of a plan item, not a definition — and a <standardEvent>.

Orvanta’s engine executes exactly two standard events:

EventCorpusEmitted by
complete669 of ~1,005 on-partsAny plan item finishing normally
occur307Milestones and user event listeners

Together that is ~97% of every on-part in the corpus. Everything else — exit (12), terminate (11), create (3), start (1), and the fourteen values with zero corpus usage — is refused at deploy with the sentry id and the offending event name. So is every caseFileItemOnPart, and every value from CMMN’s separate CaseFileItemTransition enumeration (addChild, update, …).

An on-part may also carry @exitCriterionRef, which narrows the watch to one specific exit criterion of the source rather than any exit. Orvanta executes that.

This is the rule that decides how you compose guards, and it is not configurable:

  • All the on-parts of one sentry must be satisfied for that sentry to fire. A sentry with two on-parts is an AND.
  • Any one criterion on a plan item firing is enough. A plan item with two entry criteria is an OR.

Orvanta executes both.

So to express “start when A completes and B completes”, put two on-parts in one sentry. To express “start when A completes or B completes”, put two entry criteria on the plan item, each with its own single-on-part sentry. There is no third option, and no operator to change it — the shape of the model is the boolean.

If the sentry also carries an if-part, the if-part is ANDed with the on-parts: everything in one sentry must hold.

An if-part (<ifPart>, 230 corpus occurrences) adds a boolean condition on case data. Its <condition> is a JUEL expression.

Orvanta implements a closed, decidable subset of JUEL and refuses anything outside it at deploy time:

DecidableExample
Boolean, number, string literal${true}, ${42}, ${'open'}
Bare variable path${approved}, ${order.total}
The CMMN built-in${cmmn:isStageCompletable()}
Comparison${order.total > 100}, ${status == 'open'}
Boolean algebra${a && (b || !c)}
EL emptiness${empty items}, ${not empty items}

Refused: every method call other than cmmn:isStageCompletable(), every arithmetic operator, indexed access (a[0]), the ternary, and any text outside a single ${…} / #{…} wrapper. The refusal quotes the offending expression text back, so it is actionable.

${…} and #{…} are treated identically — in Jakarta EL they differ in when a container evaluates them, and inside a case engine every condition is evaluated when the engine asks, so the distinction has no referent.

An if-part the engine cannot evaluate has no safe default. Defaulting it to false strands the sentry forever; defaulting it to true fires it unconditionally. Both look like a working case and neither is. Refusing at deploy, naming the element and quoting the expression, is the only outcome an author can act on.

This is the single largest thing standing between Orvanta and the Flowable corpus: 107 of 967 documents are blocked at deploy on an undecidable sentry if-part.

A non-boolean expression result is false — not JavaScript truthiness, not an error. Flowable’s evaluateSentryIfPart reads if (result instanceof Boolean) return (Boolean) result; and otherwise returns false, and Orvanta implements that rule.

The place this bites: ${someString} is false however non-empty the string is. If you mean “this variable is set”, write ${not empty someString}.

Trigger modes — the one that causes real bugs

Section titled “Trigger modes — the one that causes real bugs”

A sentry has a trigger mode, and the two modes disagree about memory.

Satisfied on-parts latch. When an on-part’s event fires, the sentry remembers it across evaluation cycles. The sentry fires as soon as the last outstanding on-part is satisfied — even if the first one fired an hour earlier.

This is what you want almost always, and it is why “A completes, then much later B completes, then the sentry fires” works.

On-parts do not latch. Every on-part and the if-part must hold within one evaluation cycle — the instant the connector fires.

The consequence is sharp: an onEvent sentry whose if-part is false at the moment the event arrives never fires at all. It does not wait for the condition to become true later. The event is gone.

Orvanta executes both modes.

The honest answer is: when you specifically want no memory. The common case is repetition — see Fundamentals 4, where eventDeferred’s latch is exactly the thing that causes an unintended re-activation.

An exit criterion carries two attributes that decide what the exit means. Orvanta executes both, with all their values.

@exitEventType — which lifecycle event downstream sentries see when this exit fires:

ValueDownstream sees
exit (default)exit
completecomplete — the item is treated as having finished normally
forceCompletecomplete, regardless of whether children finished

This matters because a sentry watching complete will not see a plain exit. If you exit a stage and expect the next stage to start on the stage’s complete event, set exitEventType="complete".

@exitType — which of a stage’s children the exit takes with it:

ValueTakes
defaultEverything not already in an end state
activeInstancesOnly children in active
activeAndEnabledInstancesChildren in active or enabled

Entry criteria are only re-evaluated in two states

Section titled “Entry criteria are only re-evaluated in two states”

The engine looks at a plan item’s entry criteria only when the instance is in available or wait_repetition. In every other state — including completed, active and disabled — the criteria are not consulted.

This is why a completed task does not restart when its sentry fires again, and it is the mechanism behind repetition: a repeating item that goes to wait_repetition is back in an entry-criteria-evaluating state, and will start again when its sentry next fires.

On-parts outside complete / occurRefused at deploy, naming the sentry and the event
caseFileItemOnPartRefused — the Case File model is out of scope
An if-part outside the decidable subsetRefused at deploy, quoting the expression

Fundamentals 4: repetition — doing a thing more than once, and the trigger-mode interaction.