States
Your coffee machine has structure – parts, ports, connections – and behavior – the BrewCycle action that grinds, heats, extracts, and dispenses. But an action describes what happens. It does not describe when the machine is allowed to do it.
A real coffee machine has operating modes. It sits idle until someone presses a button. It brews, then returns to idle. It enters a cleaning cycle periodically. It shuts down into an error mode when something goes wrong. These modes govern which actions are permitted and which transitions between modes are allowed.
SysML v2 models operating modes with states and transitions. This chapter adds a MachineStates state definition to the coffee machine, covering state definitions and usages, transitions with guards and triggers, entry and exit actions, and nested states.
Defining States
A state definition declares a reusable operating mode, the same way a part definition declares a reusable component type. The pattern should look familiar:
state def Idle;
state def Brewing;
These are definitions – blueprints for modes that can be used inside a larger state machine. By themselves, they do not say which system uses them or how they connect.
To build a state machine, you create a composite state definition that contains state usages and transitions between them:
state def MachineStates {
state idle : Idle;
state brewing : Brewing;
transition first idle then brewing;
}
The pattern mirrors what you saw with parts. state def Idle defines a type. state idle : Idle creates a usage of that type inside MachineStates. The transition says: from idle, proceed to brewing.
The keyword first in transition first idle then brewing identifies the source state. The keyword then identifies the target. Read it as: “transition, starting from idle, then go to brewing.”
Building the Coffee Machine States
The coffee machine needs four operating modes: idle (waiting for input), brewing (making coffee), cleaning (maintenance cycle), and error (fault detected). Start with the definitions and a containing state machine:
package CoffeeMachineStates {
state def Idle;
state def Brewing;
state def Cleaning;
state def Error;
state def MachineStates {
state idle : Idle;
state brewing : Brewing;
state cleaning : Cleaning;
state error : Error;
transition first idle then brewing;
transition first brewing then idle;
transition first idle then cleaning;
transition first cleaning then idle;
}
}
Here is what this state machine looks like in an interactive state-transition view. Each state is a rounded block, and the arrows show which transitions are possible:
This is a valid state machine, but it is too permissive. Nothing prevents the machine from transitioning to brewing when there is no water, or from leaving error without intervention. Every transition fires unconditionally. You need guards.
Guards
A guard is a boolean condition on a transition. The transition only fires when the guard evaluates to true. In SysML v2, you write a guard with the if keyword after the source state:
transition first <source> if <condition> then <target>;
Add guards to the coffee machine transitions. Assume the machine has boolean attributes that represent sensor readings:
package CoffeeMachineStates {
import ScalarValues::*;
attribute def StartBrewSignal;
attribute def StartCleanSignal;
state def Idle;
state def Brewing;
state def Cleaning;
state def Error;
state def MachineStates {
attribute waterAvailable : Boolean;
attribute cupDetected : Boolean;
attribute brewComplete : Boolean;
attribute cleanComplete : Boolean;
attribute faultDetected : Boolean;
state idle : Idle;
state brewing : Brewing;
state cleaning : Cleaning;
state error : Error;
transition first idle
if waterAvailable and cupDetected
then brewing;
transition first brewing
if brewComplete
then idle;
transition first idle
if cleanComplete == false
then cleaning;
transition first cleaning
if cleanComplete
then idle;
transition first idle
if faultDetected
then error;
transition first brewing
if faultDetected
then error;
}
}
Now the idle-to-brewing transition only fires when water is available and a cup is detected. The brewing-to-idle transition only fires when the brew cycle has completed. And any state can reach error when a fault is detected.
Guards make transition intent explicit. A reviewer reading this model can see exactly what conditions must hold for each mode change. This is especially valuable for safety-critical transitions – you do not want the machine to start brewing with no cup in place.
Triggers
Guards check conditions, but they do not say what event initiates the transition. A trigger specifies the event that causes a transition to fire. In SysML v2, triggers use the accept keyword:
transition first <source> accept <event> then <target>;
The event is typically an attribute definition that represents a signal. You define the signal type, then reference it in the transition:
attribute def StartBrewSignal;
state def MachineStates {
state idle : Idle;
state brewing : Brewing;
transition first idle
accept StartBrewSignal
if waterAvailable and cupDetected
then brewing;
}
Read this as: “When the machine is in idle and receives a StartBrewSignal, if water is available and a cup is detected, transition to brewing.”
Triggers and guards work together. The trigger says what event to respond to. The guard says whether the response is allowed. You can have a trigger without a guard (always respond to the event), a guard without a trigger (transition when the condition becomes true), or both.
Time-driven triggers: accept after
Some transitions fire on the passage of time rather than on an external signal. The spec gives you accept after(<duration>) for that case, with duration literals using SI time-units:
transition first heating accept after(30 [s]) then ready;
transition first cooling accept after(500 [ms]) then off;
transition first warming accept after(t_dead) then idle; // any Real attribute typed as a duration
The unit literal can be s, ms, us, ns, min, h — anything in the Time quantity kind from the ISQ standard library. The duration argument is a real expression: a literal like 30 [s], a model attribute like t_dead, or a calc result.
accept after is one member of a small family of trigger constructors from the spec stdlib (Triggers.kerml). The other two move the triggering event off the passage of time and onto a moment or a condition.
Condition triggers: when
A condition trigger fires when a Boolean condition becomes true, rather than when an external signal arrives. The spec constructor is TriggerWhen; in the textual form you write it on the transition as a when clause:
transition first <source> accept when <condition> then <target>;
Stand down the brewer once the water is hot enough:
transition first brewing accept when waterTemp >= 92.0 then ready;
accept when waterTemp >= 92.0 says: while the machine is in brewing, watch waterTemp; the instant the condition changes from false to true, the transition fires. Note the difference from a guard. A guard (if) is evaluated at the moment a trigger event occurs and decides whether the response is allowed. A when trigger is the event – it watches the condition continuously and fires the transition the moment it becomes true, with no external signal involved.
sysml-rs extends the spec here. The
whenconstruct is spec language. What the runtime adds is the precision: when the condition mentions a continuously-varying state variable, the runtime watches that variable and fires the transition the instant the guard becomes true, with sub-tick precision via zero-crossing detection. The spec says the condition must become true; sysml-rs’s runtime says exactly when. See Chapter 16.
Absolute-time triggers: at
after is relative – d time units elapse in the source state. at is absolute – the transition fires at a specific moment on the clock. The spec constructor is TriggerAt:
transition first <source> accept at <time-expression> then <target>;
Start the clean cycle on a schedule:
attribute cleanTime : Time;
transition first idle accept at cleanTime then cleaning;
accept at cleanTime says: when the machine is in idle, fire the transition at the clock instant named by cleanTime. cleanTime is a point on the timeline, not a duration – so at is what you reach for when the trigger is a deadline or a schedule, where after is what you reach for when the trigger is “wait, then go”.
Transitions on port messages
Every trigger so far listens for a signal by name. But the signal can also be a message arriving on a specific port. The via keyword (the same one you saw on action send/accept in Chapter 7) routes the trigger to a particular channel:
transition first <source> accept <name> : <Signal> via <port> then <target>;
Suppose the brewer exposes a dispensePort and emits CupReady on it when a cup is in place. The machine dispenses only when that specific port reports readiness:
port def DispensePort {
out item ready : CupReady;
}
part def Brewer {
port dispensePort : DispensePort;
}
transition first brewing
accept cup : CupReady via brewer.dispensePort
then dispensing;
accept cup : CupReady via brewer.dispensePort says: wait for a CupReady message arriving on brewer.dispensePort, and bind it to cup when it arrives. The via <port> clause makes the trigger precise about which connection carries the message, instead of listening for any CupReady in the machine’s namespace.
Port-message triggers pay off in component architectures where several parts emit the same signal type on different ports; via is how you say which one this transition is waiting on.
Entry actions as full expressions
The body of an entry or exit action does not have to be a plain reference to an action declared elsewhere. It can be an inline action with a body of its own – an assignment, a nested step, or a sequence of these – so you can drive state-local computation from the state itself rather than through a separate action def.
state Brewing {
entry action setPower { assign brewer.heaterPower := 1500.0; }
exit action clearPower { assign brewer.heaterPower := 0.0; }
}
When the entry/exit logic is reused across states, lift it into an action def. When it is a single in-place setting or a one-line calc, write it inline.
Entry and Exit Actions
When the machine enters a state, it often needs to do something – turn on a heater, start a timer, display a status message. When it leaves a state, it may need to clean up – turn off a pump, reset a counter. SysML v2 models these with entry and exit actions inside a state.
action def LogMessage;
action def StartHeating;
action def StopHeating;
state def Brewing {
entry action : StartHeating;
exit action : StopHeating;
}
The entry action runs every time the machine enters the Brewing state. The exit action runs every time it leaves. These are guarantees: no matter which transition enters Brewing, the heater starts. No matter which transition leaves, the heater stops.
There is also a do action, which runs continuously while the state is active:
action def MonitorTemperature;
state def Brewing {
entry action : StartHeating;
do action : MonitorTemperature;
exit action : StopHeating;
}
The do action is the ongoing behavior of the state. Think of entry as setup, do as the main work, and exit as teardown.
The Full Coffee Machine State Model
Putting it all together – states, transitions with guards and triggers, and entry/exit actions:
package CoffeeMachineStates {
import ScalarValues::*;
// Signal definitions for triggers
attribute def StartBrewSignal;
attribute def StartCleanSignal;
attribute def FaultSignal;
attribute def ResetSignal;
// Action definitions for entry/exit
action def LogStatus;
action def StartHeating;
action def StopHeating;
action def RunCleanCycle;
action def StopCleanCycle;
action def DisableOutputs;
// State definitions with entry/exit behavior
state def Idle {
entry action : LogStatus;
}
state def Brewing {
entry action : StartHeating;
exit action : StopHeating;
}
state def Cleaning {
entry action : RunCleanCycle;
exit action : StopCleanCycle;
}
state def Error {
entry action : DisableOutputs;
}
// The composite state machine
state def MachineStates {
attribute waterAvailable : Boolean;
attribute cupDetected : Boolean;
attribute brewComplete : Boolean;
attribute cleanComplete : Boolean;
state idle : Idle;
state brewing : Brewing;
state cleaning : Cleaning;
state error : Error;
// Start in idle
entry; then idle;
// Idle -> Brewing: user requests brew, preconditions met
transition first idle
accept StartBrewSignal
if waterAvailable and cupDetected
then brewing;
// Brewing -> Idle: brew cycle finished
transition first brewing
if brewComplete
then idle;
// Idle -> Cleaning: user requests cleaning
transition first idle
accept StartCleanSignal
then cleaning;
// Cleaning -> Idle: clean cycle finished
transition first cleaning
if cleanComplete
then idle;
// Any operational state -> Error: fault detected
transition first idle
accept FaultSignal
then error;
transition first brewing
accept FaultSignal
then error;
transition first cleaning
accept FaultSignal
then error;
// Error -> Idle: operator resets
transition first error
accept ResetSignal
then idle;
}
}
Several things to notice:
entry; then idle;at the top ofMachineStatesdeclares the initial state. When the state machine starts, it entersidlefirst.- Each state definition carries its own entry/exit behavior.
Brewingalways starts the heater on entry and stops it on exit, regardless of which transition enters or leaves. - The
FaultSignaltransitions are repeated for each source state. In SysML v2, each transition has exactly one source and one target. If three states can reacherror, you write three transitions. - The
ResetSignaltransition fromerrorback toidleis the only way out of the error state. This is a deliberate design choice – the operator must explicitly acknowledge and reset the fault.
Nested States
States can contain substates. When the machine is brewing, it might go through sub-modes: heating, extracting, and dispensing. You model this by putting state usages inside a state definition:
state def Brewing {
entry action : StartHeating;
state heating;
state extracting;
state dispensing;
entry; then heating;
transition first heating
then extracting;
transition first extracting
then dispensing;
exit action : StopHeating;
}
From the outside, the machine is in Brewing. From the inside, it moves through heating, extracting, and dispensing as substates. When the final substate completes, the Brewing state itself completes, and the outer state machine transitions to whatever follows.
Nested states are useful when a single operating mode has internal phases, but the rest of the system only needs to know about the outer mode. Keep nesting shallow – one level of substates covers most real systems. Deep nesting makes models hard to review.
Parallel States
Recall the closing remark from the full model above: “The FaultSignal transitions are repeated for each source state. If three states can reach error, you write three transitions.” That repetition is not just tedious – it is fragile. Add a fourth operational state, and you have to remember to give it a FaultSignal transition too. Miss one, and your machine can sit in that state forever ignoring a fault.
The reason this hurts is that the model treats a fault as something each operational mode has to anticipate individually. But a fault is not a property of brewing or cleaning – it is a property of the machine as a whole, one that should be able to interrupt whichever mode is active. SysML v2 has a construct for exactly this: a state that contains parallel regions – independent sub-machines that run concurrently inside one parent state.
Defining orthogonal regions
A region is written as a state usage nested inside a composite state, just like the nested substates you have already seen. Two things are different: you include more than one, each with its own initial transition, and you mark the parent parallel. That keyword is not decoration, and the specification is explicit about what it switches: with isParallel true the owned states are all performed in parallel, and with it false only one owned state may be performed (SysML §8.3.18.5):
state def <Parent> parallel {
state <regionA> {
/* one sub-machine: states + transitions + its own entry; then ... */
}
state <regionB> {
/* a second sub-machine, running concurrently with regionA */
}
}
The parent state def contains two named sub-machines. Entering the parent enters both; neither waits on the other. Read it as: “while the machine is in <Parent>, region A and region B are each doing their own thing, in parallel.”
One constraint comes with parallel, and the specification names it. Under validateStateDefinitionParallelSubactions (SysML §8.3.18.5): if a state definition is parallel, its owned actions – which includes its owned states – must have no incoming and no outgoing transitions. Every transition a region declares stays inside that region. This is what makes the regions orthogonal – you cannot transition from a substate of one region to a substate of another, which is exactly the independence the next section relies on.
Splitting the fault out of the operational modes
Apply this to the coffee machine. Give MachineStates two regions: an operational region that runs idle, brewing, and cleaning, and a supervision region that just listens for a fault and can move the whole machine to error:
state def MachineStates parallel {
attribute waterAvailable : Boolean;
attribute cupDetected : Boolean;
// Region 1: the operational lifecycle
state operational {
state idle : Idle;
state brewing : Brewing;
state cleaning : Cleaning;
entry; then idle;
transition first idle
accept StartBrewSignal
if waterAvailable and cupDetected
then brewing;
transition first brewing if brewComplete then idle;
transition first idle accept StartCleanSignal then cleaning;
transition first cleaning if cleanComplete then idle;
}
// Region 2: fault supervision, running concurrently
state supervision {
state armed;
state error : Error;
entry; then armed;
transition first armed accept FaultSignal then error;
transition first error accept ResetSignal then armed;
}
}
Now the fault lives in one place. The supervision region has a single FaultSignal transition, regardless of how many operational modes exist in the other region. Add a fifth or sixth operational state to the operational region and you do not touch the fault handling at all.
The trade-off is that the two regions are independent. A region does not know which substate of its sibling is active. If the fault recovery needed to behave differently depending on whether the machine was brewing or cleaning, a single supervision region would not be enough – you would be back to per-mode transitions (or you would model that knowledge a different way). Reach for parallel regions when a concern truly cuts across all the sibling substates, not when it branches on them.
Completion
SysML v2 has no final marker on a state. What it has instead is a pair of boundary states that every state inherits. States::StateAction, the standard library’s base type for every state definition, declares ref state start and ref state done – the starting and ending snapshots of the state’s performance. You mark completion by transitioning to done:
state def OneShot {
state warmup;
state ready;
entry; then warmup;
transition first warmup accept ReadySignal then ready;
transition first ready then done;
}
The last transition says: once ready has been reached, the enclosing OneShot state is finished. That is the same shape the standard library uses for its own state machines – Actions.sysml writes transition aTransition first start accept apayload: Anything via receiver then done;. start and done are ordinary inherited features, so you reference them by name like any other substate; there is no keyword to remember.
In the MachineStates model above, nothing transitions to done, so the machine never completes on its own – which is what you want for a machine that runs until it is switched off.
Resuming an interrupted region
When a region is interrupted – by a fault, by its parent being exited and re-entered, or by a transition that points at the composite from the outside – it restarts from its initial substate. Sometimes that is wrong. A brew interrupted by a clean cycle should, on returning to brewing, pick up where it left off, not restart grinding.
SysML v2 has no history pseudostate. If you are coming from SysML v1 or UML, this is one of the constructs that did not carry over: there is no shallow or deep history marker in the language, and no library element that stands in for one. Resuming is something you model explicitly.
The straightforward way is to keep the last active substate in an attribute and branch on it when the region is re-entered:
state def BrewPhases {
attribute resumeAt : String;
state dispatch;
state grinding { entry action markGrinding { assign resumeAt := "grinding"; } }
state extracting { entry action markExtracting { assign resumeAt := "extracting"; } }
entry; then dispatch;
// Re-entry branches on the recorded phase instead of always restarting
transition first dispatch if resumeAt == "extracting" then extracting;
transition first dispatch if resumeAt != "extracting" then grinding;
transition first grinding accept PhaseDone then extracting;
}
Each phase records that it was entered; a small dispatch state reads that record and routes to the right phase using ordinary guarded transitions. It is more verbose than a history marker, but the state being remembered is now a visible part of the model – which is what a reviewer needs anyway. Deep history has no shorthand either: nest the same pattern, one attribute per level.
sysml-rs extends the spec here. Parallel regions are spec –
parallelis a real modifier on a state definition or usage. What the runtime adds on top is the scheduling: when an event arrives, the runtime advances each region to a stable point before delivering the next external event, so every region sees a consistent snapshot of the others. That run-to-completion guarantee is the sysml-rs execution rule; the spec says the regions are performed concurrently, not how a tool must drain their internal queues.One caveat if you are following along with the tool: sysml-rs does not yet parse the
parallelkeyword, so the model above will not check. The keyword is specification syntax and the example is correct – the gap is on the tool’s side.
Completion Transitions
Every transition so far has an explicit trigger – a signal, a duration, a condition, a port message. But a state that runs a do behavior has one more way to leave: it can finish. A completion transition fires when the source state’s do behavior completes, with no trigger written at all:
transition first <source> then <target>;
The absence of a trigger is the trigger. The Brewing state from the nested-states section runs MonitorTemperature as its do action; when that action ends, the state is done, and a completion transition can carry the machine onward. Like every transition, it connects state usages, so it lives in the containing state machine:
state def Brewing {
entry action : StartHeating;
do action : MonitorTemperature;
exit action : StopHeating;
}
state def Dispensing;
state def BrewSequence {
state brewing : Brewing;
state dispensing : Dispensing;
transition first brewing then dispensing; // fires when brewing's `do` completes
}
Read it as: “when the brewing state is done, go to dispensing.” No accept, no when, no after – the completion of the do behavior is the implicit event.
Completion is stricter in a parallel composite: the parent is not finished while any region is still running. Since there is no final marker in the language, a region signals that it is finished the way any state does – by transitioning to its inherited done state, as in the OneShot example above.
Exhibiting State Machines on Parts
So far the state machine has lived in its own state def MachineStates, separate from the parts it governs. SysML v2 also lets a part carry its own state machine inline, with the exhibit keyword.
part def <Part> {
exhibit state <name> { <states and transitions> };
}
Give the brewer its own small lifecycle, owned by the part rather than declared outside it:
part def Brewer {
exhibit state brewingMode {
state heating;
state ready;
entry; then heating;
transition first heating accept when waterTemp >= 92.0 then ready;
}
}
exhibit state brewingMode { ... } says: every Brewer instance runs its own brewingMode state machine. The states and transitions live inside the part definition, so a reviewer reading Brewer sees the part’s structure and its lifecycle in one place.
Exhibiting is the structural counterpart to the parallel regions from earlier. Parallel regions live inside a composite state and share one machine. An exhibit lives on a part and is owned by that part – the part’s internal modes are its own API, and other parts reach them by sending signals to that part. Reach for exhibit when a component’s modes are nobody else’s business; reach for a shared state def when the modes are a property of the system the parts belong to.
States and Actions Together
States and actions serve different roles. Actions describe what happens – the sequence of steps in a brew cycle. States describe when things are allowed – the machine must be in Idle before it can start brewing.
In a complete model, you connect them. The do action inside a state can reference an action definition:
action def BrewCycle {
action grind : GrindBeans;
action heat : HeatWater;
action extract : ExtractCoffee;
action dispense : DispenseCoffee;
first grind then heat then extract then dispense;
}
state def Brewing {
entry action : StartHeating;
do action : BrewCycle;
exit action : StopHeating;
}
When the machine enters Brewing, it runs StartHeating, then executes BrewCycle as its ongoing behavior, and runs StopHeating when it leaves. The state governs the lifecycle; the action governs the procedure.
sysml-rs extends the spec here. A state machine can also gate continuous behavior, not just discrete actions. When a model carries an ODE — see Chapter 9’s
:> GetDerivativepattern and the worked hybrid example in Chapter 16 — entering one state can switch which derivative is integrated. The runtime monitors transition guards that mention continuous state variables and fires those transitions the instant the guard becomes true, with sub-tick precision. This is what lets a thermostat trip cleanly at 100°C and a bouncing ball bounce at exactly y=0. The state-machine half is spec; the zero-crossing-as-transition mechanism is sysml-rs’s runtime contribution.
Common Mistakes
Forgetting the initial state. A composite state definition without entry; then <state>; has no defined starting point. Reviewers and tools will not know which substate activates first. Always declare the initial transition.
Omitting guards on safety-critical transitions. An unguarded transition from idle to brewing means the machine can start brewing at any time – no water check, no cup check. If a transition has safety implications, give it a guard. The guard makes the precondition visible in the model, not hidden in implementation code.
Using states where actions belong. If a “state” has no transitions into or out of it and just runs a fixed sequence of steps, it is probably an action, not a state. States model modes that the system can be in. Actions model procedures that the system performs. The test is: can the system stay in this mode indefinitely, waiting for an event? If yes, it is a state. If it always runs to completion, it is an action.
What You Have Built
With the state definitions expanded, you can see the entry and exit actions inside each state, plus all the transitions with their guards and triggers:
The coffee machine now has a complete lifecycle model. MachineStates defines four operating modes with guarded, triggered transitions between them. Entry and exit actions ensure consistent setup and teardown. The initial state is explicit, the error state requires a deliberate reset, and the guard on idle to brewing enforces preconditions that a reviewer can inspect.
In Chapter 9, you will add constraints – checkable logic that makes properties like temperature ranges and safety interlocks part of the model itself.