A guard decides whether one otherwise matching transition is enabled for the values and active
facts available now. Attach that synchronous decision with .when.
A valid event can still be refused.
A decoded event answers whether the stimulus is well formed. A guard answers whether this event may take this edge, using the facts active now.
1 · Shape is not permission
Every string opens the keypad.
EnterCode admits a string, but this edge accepts every admitted value. Even a wrong code is structurally valid, so the chart enters Unlocked.
import { State, Statechart, States, Transitions } from "@motive/motive";
import * as Effect from "effect/Effect";
import { Schema } from "effect";
const EnterCode = Schema.TaggedStruct("EnterCode", {
code: Schema.String,
});
class Keypad extends State.Compound<Keypad>()("Keypad", {
accessCode: Schema.String.pipe(
Schema.withConstructorDefault(Effect.succeed("1234")),
),
}) {
static states = States.make(() => [Locked, Unlocked]);
}
class Locked extends State.Atomic<Locked>()("Locked") {
static transitions = Transitions.make(this, ({ on }) => [
on(EnterCode, Unlocked),
]);
}
class Unlocked extends State.Atomic<Unlocked>()("Unlocked") {}
export const KeypadChart = Statechart.make(Keypad);
2 · Guard the edge
A valid event can still be refused.
The guard asks a second question after decoding: does this particular code permit this edge? A wrong code leaves Locked active; 1234 enters Unlocked.
import { State, Statechart, States, Transitions } from "@motive/motive";
import * as Effect from "effect/Effect";
import { Schema } from "effect";
const EnterCode = Schema.TaggedStruct("EnterCode", {
code: Schema.String,
});
class Keypad extends State.Compound<Keypad>()("Keypad", {
accessCode: Schema.String.pipe(
Schema.withConstructorDefault(Effect.succeed("1234")),
),
}) {
static states = States.make(() => [Locked, Unlocked]);
}
class Locked extends State.Atomic<Locked>()("Locked") {
static transitions = Transitions.make(this, ({ on }) => [
on(EnterCode, Unlocked).when(({ event }) => event.code === "1234"),
]);
}
class Unlocked extends State.Atomic<Unlocked>()("Unlocked") {}
export const KeypadChart = Statechart.make(Keypad);
3 · Read the active world
The decision reads its real owner.
Query reads the current EnterCode and the active Keypad data together. The guard no longer repeats the access code, so one state-owned fact governs the decision.
import { Query, State, Statechart, States, Transitions } from "@motive/motive";
import * as Effect from "effect/Effect";
import { Schema } from "effect";
const EnterCode = Schema.TaggedStruct("EnterCode", {
code: Schema.String,
});
const codeMatches = Query.gen(function* () {
const event = yield* Query.event(EnterCode);
const keypad = yield* Keypad;
return event.code === keypad.accessCode;
});
class Keypad extends State.Compound<Keypad>()("Keypad", {
accessCode: Schema.String.pipe(
Schema.withConstructorDefault(Effect.succeed("1234")),
),
}) {
static states = States.make(() => [Locked, Unlocked]);
}
class Locked extends State.Atomic<Locked>()("Locked") {
static transitions = Transitions.make(this, ({ on }) => [
on(EnterCode, Unlocked).when(codeMatches),
]);
}
class Unlocked extends State.Atomic<Unlocked>()("Unlocked") {}
export const KeypadChart = Statechart.make(Keypad);
Shape and permission answer different questions
EnterCode admits an object with one string field. Its Schema rejects malformed input, but every
string—including "0000"—has the right shape.
The first chart has no guard. Every decoded EnterCode matches the Locked transition, so a
well-formed wrong code enters Unlocked. Schema admission proves that the event is valid data; it
does not prove that this transition should run.
A guard narrows one transition
The second frame adds .when to the existing on declaration:
on(EnterCode, Unlocked).when(({ event }) => event.code === "1234");
The callback receives the decoded EnterCode, so event.code is already a string. Returning
true enables this edge for the current step. Returning false refuses the edge and leaves the
active configuration unchanged.
A guard is synchronous and pure. It decides whether authored behavior is eligible; it does not perform an Effect, mutate state, send another event, or retry work.
Read the fact from its owner
Hard-coding "1234" makes the intermediate chart behave correctly, but it gives the access code a
second owner. Keypad.accessCode already owns that fact.
The final frame names a query guard:
const codeMatches = Query.gen(function* () {
const event = yield* Query.event(EnterCode);
const keypad = yield* Keypad;
return event.code === keypad.accessCode;
});
Query.event(EnterCode) reads the accepted event for this step. yield* Keypad reads the active
state facet that owns the configured access code. The query returns one boolean and retains both
requirements for the .when placement checker.
Placement proves the strict read
The guard is attached to a transition owned by Locked, a child of Keypad. Whenever that edge
can be considered, Keypad is provably active, so its strict facet read is valid.
Moving the same guard to a site that cannot prove Keypad active would be rejected during chart
assembly. Guard evaluation does not turn missing state into undefined or defer a topology mistake
until production.
Refusal preserves the world
Submit 0000 in the second or third frame. The event is decoded, but the guard returns false.
The send result is Refused and Locked remains active.
Then submit 1234. The same event Schema now satisfies the guard, the result is Handled, and the
chart enters Unlocked. Admission, permission, and the resulting configuration are three distinct
facts.
If several transitions handle the same event, guards can make different edges eligible. Selection still follows the chart's authored transition semantics; a guard only contributes its boolean decision.
Test the decision
- Change the state-owned access code and predict which attempt the final chart accepts.
- Add a second guarded edge for a maintenance code and give it a different target.
- Replace the strict
yield* Keypadread withyield* Query.option(Keypad)and handle the resultingOptionexplicitly.