Skip to content
Leon Starr edited this page Aug 4, 2026 · 3 revisions

Every event that this state model can receive is declared up front, in one block, before any states are defined. The block has two sections, naming how the event reaches the state machine.

Here is the events block from the Door lifecycle:

interaction events
    Door opened
    Passenger open
    Passenger close
    Time to close
    Hold released
    Door closed
    Lock
    Unlock
    Door blocked
completion events
    Open delay canceled
    Keep trying
    Cannot close door
--

And the grammar:

// Events
events = interaction_events? completion_events? block_end
interaction_events = interaction_events_header event_spec*
interaction_events_header = "interaction events" EOL*
completion_events = completion_events_header event_spec*
completion_events_header = "completion events" EOL*
event_spec = INDENT event_name EOL*
event_name = name

An unindented interaction events or completion events keyword opens each section, and each event follows on its own indented line. One unindented -- closes the whole block, not one per section — the two headers are sections of a single events block, the same way transitions, ignore and can't happen are sections of a single state block. See State block grammar.

Both sections are optional. A state model with nothing but interaction events simply has no completion events header.

An event name is just a name — see Names — so Time to close and Cannot close door are each a single event, not three or four words of something. My convention is to name events for what has happened, in the past tense where it reads naturally (Door opened, Door closed) or as a directive when one object is telling another to do something (Lock, Unlock, Keep trying).

Interaction or completion?

A completion event signals that a state's activity has finished so the state machine can move on. It is generated by a state's own activity and directed at itself:

state Count block
activity
    Close attempts.increment
    (Close attempts > /R4/R2/Bank.Max close attempts) ? Cannot close door -> me : {
        Blocked.set
        Keep trying -> me
    }

Keep trying and Cannot close door exist only to carry Count block forward to its next state. Nothing outside the Door lifecycle ever sends them, so they are declared as completion events.

An interaction event arrives from somewhere else — another state model, another domain, an external entity. Door opened comes from the physical door, Lock from the Cabin.

The rule when an event is both

Most events are clearly one or the other, but nothing stops a state from generating an event that can also arrive from outside. The Door's Hold released is exactly that case:

state HOLDING OPEN
activity
    !Held ? Hold released -> me
transitions
    Hold released > CLOSING

If the door is not being held, HOLDING OPEN releases itself immediately and that use is a completion. If it is being held, the state waits and the release arrives from outside.

An event is a completion event only when that is its sole use. Anything that can also arrive from outside is an interaction event, even where some state generates it for itself. Classification really belongs to the dispatch rather than to the event, so the broader use has to win. A state machine that treated Hold released as a completion event would be entitled to assume it can never arrive in any other state, and that is not true.

Why the distinction is worth declaring

Because it decides what a missing response means. An event with no declared response in a state can't happen — but for the two kinds of event that verdict is reached very differently:

  • A completion event cannot be received in any state other than the one that issued it. That follows from the definition, so there is nothing to explain and nothing for the modeler to decide.
  • An interaction event can arrive at any time. If a state has no declared response to one, that is a decision nobody has made yet, not a systematic fact.

Declaring the split lets tooling tell those two apart and report the second as unfinished work. Without it, an undecided response looks exactly like an impossible one.

Why declare events at all?

Because the declaration is the model's contract. Any event a state machine can receive is listed here, once, whether it's sent from another state model, from an external domain, or by the state machine to itself. That gives a downstream module a complete picture of what this state machine responds to without having to scan every state, and it gives you a place to notice that you've accidentally invented two names for the same event.

The event names appearing in each state's Transitions, Ignore and Can't Happen sections must all be drawn from this block.

What the parser gives you

Three fields on StateModel_a:

  • interaction_events and completion_events — the two sections, each a list of event names
  • events — every declared event in declaration order, interaction events first

Clone this wiki locally