Guide

on

Declare which event one active source state handles and the transition it triggers.

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

  1. Add a LockDoor event handled by Unlocked and target Locked to form a cycle.
  2. Add a second source state that handles UnlockDoor with a different target.
  3. Remove Unlocked from Door.states and verify that the transition target is no longer part of the assembled topology.