An event is necessary when new information arrives. When the chart already has every fact needed
for its next decision, always lets it continue without inventing another command.
Continue when there is nothing to wait for.
Some decisions need no new event. always lets the chart continue from the facts it already has until no eventless edge remains enabled.
1 · Wait for another event
The caller advances a decision the chart already knows.
Begin publishes Preparing, then the chart waits for Continue. The caller must send a second event even though no new fact is needed to choose Ready.
import { State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";
const Begin = Schema.TaggedStruct("Begin", {});
const Continue = Schema.TaggedStruct("Continue", {});
class Checkout extends State.Compound<Checkout>()("Checkout") {
static states = States.make(() => [Idle, Preparing, Ready]);
}
class Idle extends State.Atomic<Idle>()("Idle") {
static transitions = Transitions.make(this, ({ on }) => [
on(Begin, Preparing),
]);
}
class Preparing extends State.Atomic<Preparing>()("Preparing") {
static transitions = Transitions.make(this, ({ on }) => [
on(Continue, Ready),
]);
}
class Ready extends State.Atomic<Ready>()("Ready") {}
export const CheckoutChart = Statechart.make(Checkout);
2 · Continue without an event
always moves as soon as Preparing is active.
always(Ready) is considered as soon as Preparing becomes active. The chart settles at Ready before publishing the result, so no caller has to send Continue.
import { State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";
const Begin = Schema.TaggedStruct("Begin", {});
class Checkout extends State.Compound<Checkout>()("Checkout") {
static states = States.make(() => [Idle, Preparing, Ready]);
}
class Idle extends State.Atomic<Idle>()("Idle") {
static transitions = Transitions.make(this, ({ on }) => [
on(Begin, Preparing),
]);
}
class Preparing extends State.Atomic<Preparing>()("Preparing") {
static transitions = Transitions.make(this, ({ always }) => [always(Ready)]);
}
class Ready extends State.Atomic<Ready>()("Ready") {}
export const CheckoutChart = Statechart.make(Checkout);
3 · Settle the whole chain
Eventless transitions continue until none remain enabled.
Preparing now leads through Validating before Ready. Both eventless transitions run in order, yet readers still receive only the settled Ready configuration.
import { State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";
const Begin = Schema.TaggedStruct("Begin", {});
class Checkout extends State.Compound<Checkout>()("Checkout") {
static states = States.make(() => [Idle, Preparing, Validating, Ready]);
}
class Idle extends State.Atomic<Idle>()("Idle") {
static transitions = Transitions.make(this, ({ on }) => [
on(Begin, Preparing),
]);
}
class Preparing extends State.Atomic<Preparing>()("Preparing") {
static transitions = Transitions.make(this, ({ always }) => [
always(Validating),
]);
}
class Validating extends State.Atomic<Validating>()("Validating") {
static transitions = Transitions.make(this, ({ always }) => [always(Ready)]);
}
class Ready extends State.Atomic<Ready>()("Ready") {}
export const CheckoutChart = Statechart.make(Checkout);
Waiting should wait for something
The first Checkout enters Preparing when it handles Begin, then waits for Continue before it
can enter Ready.
But Continue carries no new information. Nothing outside the chart changed, no clock expired,
and no work completed. The sender is being asked to advance a decision that the chart already
knows how to make.
The live readout exposes the extra public stop: Begin makes Preparing visible, and Continue
is required to publish Ready.
always needs no event
The second frame replaces the Continue handler with an eventless transition:
class Preparing extends State.Atomic<Preparing>()("Preparing") {
static transitions = Transitions.make(this, ({ always }) => [
always(Ready),
]);
}
As soon as Preparing becomes active, always(Ready) is eligible. It does not wait for a sender
or consume an event. The chart continues to Ready from the facts already present.
Now one Begin produces one visible change from Idle to Ready. Preparing never becomes a
configuration that an external reader can observe and act on.
Transient states are still states
Preparing remains real authored topology. It can own data, guards, and lifecycle behavior, and
the transition through it is part of the chart's deterministic execution.
Transient means only that the state cannot be a stable stopping point while one of its eventless edges is enabled. It does not mean the state is erased from the model or replaced with an imperative function call.
The chart settles the whole chain
The final frame inserts Validating:
Preparing → Validating → Ready
Both links are always transitions. After Begin, the chart continues through the chain in
authored order and publishes only the settled Ready configuration.
Eventless transitions are reconsidered after each internal step until none remain enabled. A
guarded always edge may therefore become eligible because an earlier step changed active facts.
Stabilization must terminate
A configuration with no enabled eventless transition is stable: the chart can honestly wait for
new input. An accidental cycle made entirely of enabled always edges can never reach that point.
Motive enforces a stabilization safety boundary and fails a non-terminating automatic cycle instead of allowing one instance to run forever.
Use always when the next choice follows entirely from chart facts already present. If a person,
service, timer, activity, or child must supply new information, that arrival deserves its own
event or mounted citizen.
Test the stabilization
- Add one more transient validation state and confirm that
Beginstill reveals onlyReady. - Guard the
Preparingdecision and give its fallback a different stable target. - Point
Readyback toPreparingwith another eventless edge and observe the safety boundary.