on declares that one state accepts an event and what transition that accepted event triggers.
Event identity comes from its Schema; transition behavior belongs to the state that can respond.
Events trigger transitions between states.
An event becomes behavior only where a state handles it. on connects one admitted event to the transition that should occur from that active source.
1 · Define the vocabulary
An event alone changes nothing.
UnlockDoor is a valid decoded event, and the Door already has two states. Nothing connects them yet: neither active state has declared that it handles this event.
import { State, Statechart, States } from "@motive/motive";
import { Schema } from "effect";
export const UnlockDoor = Schema.TaggedStruct("UnlockDoor", {});
class Door extends State.Compound<Door>()("Door") {
static states = States.make(() => [Locked, Unlocked]);
}
class Locked extends State.Atomic<Locked>()("Locked") {}
class Unlocked extends State.Atomic<Unlocked>()("Unlocked") {}
export const DoorChart = Statechart.make(Door);
2 · Handle the event
Locked accepts UnlockDoor and targets Unlocked.
on(UnlockDoor, Unlocked) belongs to Locked. While Locked is active the event is accepted and triggers the authored transition; once Unlocked is active, that local handler no longer applies.
import { State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";
export const UnlockDoor = Schema.TaggedStruct("UnlockDoor", {});
class Door extends State.Compound<Door>()("Door") {
static states = States.make(() => [Locked, Unlocked]);
}
class Locked extends State.Atomic<Locked>()("Locked") {
static transitions = Transitions.make(this, ({ on }) => [
on(UnlockDoor, Unlocked),
]);
}
class Unlocked extends State.Atomic<Unlocked>()("Unlocked") {}
export const DoorChart = Statechart.make(Door);
A defined event is not a global handler
The first frame has a valid UnlockDoor event and a Door with Locked and Unlocked states. The
event is part of the domain vocabulary, but the chart has not assigned it any behavior.
That separation is deliberate. An event describes what occurred. Each state independently decides whether that occurrence is meaningful in its current situation.
States declare the events they handle
Locked owns a Transitions.make declaration. The first argument, this, identifies the source
state; the callback receives the transition authoring vocabulary available at that source.
static transitions = Transitions.make(this, ({ on }) => [
on(UnlockDoor, Unlocked),
]);
Read the declaration from left to right: while Locked is active, accept UnlockDoor and target
Unlocked.
The active source determines acceptance
Before the event, Locked is active and its outgoing UnlockDoor transition is available. Send
the event in the live view and the active configuration moves to Door.Unlocked.
Unlocked does not declare an UnlockDoor handler. The same event is therefore unavailable after
the transition. Defining a Schema did not create a global listener, and the handler did not follow
the instance into its target state.
This locality is one of the main reasons statecharts remain legible as behavior grows: the active configuration determines the handlers that can participate in the next step.
The event and target have different jobs
UnlockDoor identifies the admitted stimulus. Unlocked names the destination configuration.
Keeping those roles explicit lets the same event target different states from different sources,
or remain unhandled where it is irrelevant.
This transition has no guard and no actions. Accepting the event and changing configuration is the
entire behavior. Later pages add those concerns without changing what on means.
Sending is outside the model
The authored chart declares how an accepted event changes the instance. A client, child, timer, or other runtime source delivers the event to a particular running instance.
For a chart-owned client, the generated operation is client.send.UnlockDoor(). No argument is
required because the event Schema has no required constructor input; an event with data accepts
that Schema's structured constructor input. Delivery asks the active chart to consider the event;
the on declaration decides whether it is accepted and where it goes.
Test the transition
- Add a
LockDoorevent handled byUnlockedand targetLockedto form a cycle. - Add a second source state that handles
UnlockDoorwith a different target. - Remove
UnlockedfromDoor.statesand verify that the transition target is no longer part of the assembled topology.