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.
Criteria and sentries
Section titled “Criteria and sentries”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.
The on-part
Section titled “The on-part”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:
| Event | Corpus | Emitted by |
|---|---|---|
complete | 669 of ~1,005 on-parts | Any plan item finishing normally |
occur | 307 | Milestones 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.
AND within a sentry, OR across sentries
Section titled “AND within a sentry, OR across sentries”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.
The if-part, and the decidable subset
Section titled “The if-part, and the decidable subset”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:
| Decidable | Example |
|---|---|
| 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.
Why refusal rather than a default
Section titled “Why refusal rather than a default”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.
Truthiness follows Flowable
Section titled “Truthiness follows Flowable”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.
eventDeferred (the default)
Section titled “eventDeferred (the default)”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.
onEvent
Section titled “onEvent”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.
When to reach for onEvent
Section titled “When to reach for onEvent”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.
Exit criteria: exitType and exitEventType
Section titled “Exit criteria: exitType and exitEventType”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:
| Value | Downstream sees |
|---|---|
exit (default) | exit |
complete | complete — the item is treated as having finished normally |
forceComplete | complete, 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:
| Value | Takes |
|---|---|
default | Everything not already in an end state |
activeInstances | Only children in active |
activeAndEnabledInstances | Children 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.
What Orvanta refuses, in one list
Section titled “What Orvanta refuses, in one list”On-parts outside complete / occur | Refused at deploy, naming the sentry and the event |
caseFileItemOnPart | Refused — the Case File model is out of scope |
| An if-part outside the decidable subset | Refused at deploy, quoting the expression |
Fundamentals 4: repetition — doing a thing more than once, and the trigger-mode interaction.