${{ ... }}
parameter substitution
(build-time, used in subforms).State machines describe the lifecycle of an entity: its possible states, the transitions between them, and the side-effects (cascades) that fire on transitions.
MMEL 0.1 has
exclusive_gateway
(decision points in process flow) but
no first-class state machine construct. Side-effects on state changes
are implicit in the flow topology, not declarative.
For lifecycle-heavy domains (certification, audit, approval), you need to declare: "when this entity's state changes, these dependent entities also change." BPMN-style flowcharts aren't enough.
State machines complement
process
(which describes activities) by
describing entity state evolution.
| Construct | Relationship |
|---|---|
class#data |
A state machine is bound to a data class. The class's `status` field holds the current state. |
process |
Processes describe activities that may trigger state transitions. State machines describe the resulting state evolution. |
exclusive_gateway |
Gateways decide which process flow branch to take. State machines constrain which transitions are valid from the current state. |
provision |
A transition's guard often validates a provision. |
The distinction:
Both are needed. The activity description tells you WHO does WHAT. The lifecycle description tells you what SIDE-EFFECTS follow.
A state machine declaration uses the
state_machine
keyword followed
by an entity name (matching a
class#data
) and a brace-delimited body.
state_machine <EntityName> {
initial <StateName>
states {
<StateName>
<StateName>
// ...
}
transition <From> -> <To>
[action <ActionName>] {
guard "<OCL predicate>"
cascade <EntityName> {
where "<OCL predicate>"
set {
<field>: <value>
}
}
reference { <ReferenceID> }
}
}
| Property | Required | Description |
|---|---|---|
initial |
yes | The starting state for new entities. |
states |
no | Explicit state list. Inferred from transitions if omitted. Recommended for documentation. |
transition |
no | Transition declaration. Zero or more. |
The
<EntityName>
after
state_machine
must match a
class#data
declared elsewhere in the model. The class is expected to have a
status
field that holds the current state.
The parser:
1. Collects all
class#data
declarations
2. For each
state_machine <EntityName>
, validates that a matching
class exists
3. Emits a warning (not error) if the class lacks a
status
field
state_machine TestReport {
initial DRAFT
states {
DRAFT
SUBMITTED
UNDER_REVIEW
ACCEPTED
REJECTED
}
transition DRAFT -> SUBMITTED action lab_submits {
cascade FormInstance {
where "testReportId = ${this.id}"
set { status: "LOCKED" }
}
}
transition SUBMITTED -> UNDER_REVIEW action ia_starts_review {
}
transition UNDER_REVIEW -> ACCEPTED action ia_accepts {
guard "not some(FormInstance where testReportId = ${this.id} and result = 'FAIL')"
}
transition UNDER_REVIEW -> REJECTED action ia_rejects {
guard "some(FormInstance where testReportId = ${this.id} and result = 'FAIL')"
}
}
A transition moves an entity from one state to another. It can fire cascades (side-effects on related entities) and may be guarded by an OCL predicate.
transition <From> -> <To> [action <ActionName>] {
// body
}
A transition + its cascades execute ATOMICALLY:
1. The guard (if any) is evaluated 2. If the guard passes, the source entity's state is updated 3. All cascades execute in declaration order 4. If any cascade fails, the entire transition is rolled back
Firing the same transition on the same entity twice (without an intervening change) is an ERROR. The state machine rejects it.
Within a single state machine, no two transitions can have the same
(from, to, action)
tuple. The parser rejects duplicates.
Multiple transitions can originate from the same state (branching). At
runtime, the caller specifies which transition to fire (typically via
the
action
name). If multiple transitions from the same state would
satisfy their guards simultaneously, the caller disambiguates.
A state with no outgoing transitions is a terminal state . Entities in a terminal state cannot transition further. The state machine does not enforce that entities reach a terminal state; that's a domain concern.
transition SUBMITTED -> UNDER_REVIEW action ia_starts_review {
}
No guard, no cascades. The state simply changes.
transition UNDER_REVIEW -> ACCEPTED action ia_accepts {
guard "not some(FormInstance where testReportId = ${this.id} and result = 'FAIL')"
cascade EvaluationReport {
where "testReportIds->includes(${this.id})"
set { admissibility_progress: "incremented" }
}
reference { PD05-4-5 }
}
A cascade is a declarative side-effect that fires when a transition completes. Cascades update related entities based on a predicate and a set of field updates.
cascade <TargetEntity> {
where "<OCL predicate>"
set {
<field>: <value>
<field>: <value>
}
}
A transition can declare multiple cascades. They execute in declaration order:
transition DRAFT -> SUBMITTED action lab_submits {
cascade FormInstance {
where "testReportId = ${this.id}"
set { status: "LOCKED" }
}
cascade AuditEvent {
where "id = NULL" // always create new
set {
entityType: "TestReport"
entityId: "${this.id}"
action: "lab_submits"
timestamp: "${now}"
}
}
}
${this.<field>}
resolves to the source entity's field value:
where "testReportId = ${this.id}"
set { entityId: "${this.id}" }
${<TransitionName>.<field>}
resolves to a value produced by a named
prior cascade (future extension — for now, cascades don't produce
output values).
${now}
resolves to the current ISO-8601 timestamp at execution time.
${user}
resolves to the current user's identifier.
Cascades execute in declaration order. If cascade B depends on a value set by cascade A, A must be declared first.
If a cascade fails (e.g., a
where
predicate matches no instances,
or a
set
value violates a type constraint), the entire transition
is rolled back. The state does not change.
Failure modes:
A cascade can target the same entity as the state machine itself:
transition DRAFT -> SUBMITTED action lab_submits {
cascade TestReport {
where "id = ${this.id}"
set { submittedDate: "${now}" }
}
}
This updates the source entity's
submittedDate
field as part of the
transition.
cascade EvaluationReport {
where "id = ${this.evaluationReportId}"
set {
admissibilityProgress: "${this.admissibilityProgress + 1}"
}
}
The set value is an OCL expression evaluated against the source entity.
A guard is an OCL predicate that must evaluate to
true
for a
transition to fire. Guards are evaluated BEFORE the transition's state
change.
transition <From> -> <To> [action <ActionName>] {
guard "<OCL predicate>"
// ...
}
The OCL predicate has access to:
transition UNDER_REVIEW -> ACCEPTED action ia_accepts {
guard "not some(FormInstance where testReportId = ${this.id} and result = 'FAIL')"
}
The guard queries all FormInstances on this TestReport and ensures none
have result
FAIL
.
transition SUBMITTED -> WITHDRAWN action ia_withdraws {
guard "${this.submittedDate} > ${now} - 30 days"
cascade FormInstance {
where "testReportId = ${this.id}"
set { status: "RELEASED" }
}
}
The transition is only allowed within 30 days of submission.
Guards and
applicability
are different:
Guards evaluate at runtime; applicability evaluates at form materialization time.
A transition can have both:
transition UNDER_REVIEW -> ACCEPTED action ia_accepts {
guard "not some(FormInstance where testReportId = ${this.id} and result = 'FAIL')"
cascade EvaluationReport {
where "id = ${this.evaluationReportId}"
set { acceptedReportCount: "${this.acceptedReportCount + 1}" }
}
}
The guard is evaluated first. Only if it passes do the cascades fire.
Tokens inside
where
predicates and
set
values are substituted at
runtime, using the source entity's state and a small set of context
variables.
The token syntax is
${...}
— dollar-brace, expression, close-brace.
Note: this is DIFFERENT from the parameter substitution syntax (
${{ ... }}
,
double braces) used in subforms. The single-brace form is for runtime
values; the double-brace form is for build-time parameters.
| Token | Description |
|---|---|
${this} |
The source entity (the entity whose state is changing). |
${this.id} |
The source entity's identifier. |
${this.<field>} |
Any field on the source entity. |
${now} |
The current ISO-8601 timestamp at execution time. |
${user} |
The current user's identifier. |
Tokens can contain OCL expressions:
set { admissibilityProgress: "${this.admissibilityProgress + 1}" }
The expression is evaluated against the source entity.
Tokens are resolved by the state machine runtime, not by the parser. The parser validates that the token syntax is correct but does not evaluate the expressions.
Tokens can reference other entities via their class name:
guard "not some(FormInstance where testReportId = ${this.id} and result = 'FAIL')"
The
FormInstance
identifier resolves to the
FormInstance
class in
the current model.
The two substitution mechanisms serve different purposes:
The double-brace vs single-brace distinction prevents ambiguity.
StateMachineDecl = "state_machine" ID "{" { StateMachineProperty } "}" ;
StateMachineProperty = "initial" ID
| "states" "{" { ID } "}"
| TransitionDecl ;
TransitionDecl = "transition" ID "->" ID
[ "action" ID ] "{" { TransitionProperty } "}" ;
TransitionProperty = "guard" STRING
| CascadeDecl
| "reference" "{" { ID } "}" ;
CascadeDecl = "cascade" ID "{" CascadeProperty { CascadeProperty } "}" ;
CascadeProperty = "where" STRING
| "set" "{" { SetEntry } "}" ;
SetEntry = ID ":" STRING ;
As per the forms grammar (see formal-grammar-forms ).
Within any
STRING
value used as a
where
predicate or
set
value,
${...}
tokens are recognized. These are preserved by the parser and
resolved at runtime.
${{ ... }}
parameter substitution
(build-time, used in subforms).