Machine definitions are exhaustive records keyed by state tag. The key supplies the current-state type, so constructors do not repeat it:
const definition = machine.define(
{ id: "editor", initial },
{
Typing: machine.state(events, { after }),
Saving: machine.invoke(work, events),
Closed: machine.final(),
},
)Reducers return the destination state's fields without _tag. The interpreter assigns the
declared target after the reducer runs and validates the resulting value with the state Schema.
This keeps the transition declaration authoritative even if a spread or excess return property
contains a conflicting _tag.
Use stay to update the active value without leaving its current entry:
Edit: { stay: ({ event }) => ({ text: event.text }) }A stay preserves entry-owned work and timers. A transition whose target is the current tag is different: it exits and re-enters the node, interrupting work and restarting its timer. That makes an explicit self-target useful for debounce:
Edit: { target: "Typing", reduce: ({ event }) => ({ text: event.text }) }A non-final node may own one after timer. The timer starts on entry, is cancelled on exit, and is
protected by the same stale-entry checks as work outcomes. A duration may be a static
Duration.Input or named synchronous logic computed once from the entry state. When it fires,
after may select one direct target or an ordered guarded branch using the latest state value.
Region timers support the same forms with child state and parent state arguments.
invoke runs one Effect whose success and error Schemas define its replayable outcome
contract. invoke.all joins a keyed product and can limit concurrency; every named lane is an
object with its own success, error, and effect. Its first typed failure interrupts unfinished
siblings. invoke.race returns a correlated winner and
value; typed lane failures do not end the race while another lane can still succeed. Its failure
transition runs only after every lane has failed, using the final observed typed failure. Defects
remain defects and never enter a typed failure reducer.
regions declares one compound slot or several parallel slots directly on their owning state:
Active: player.regions(
{
playback: {
Playing: {
Pause: { target: "Paused", reduce: ({ event }) => ({ position: event.position }) },
},
Paused: { Resume: { target: "Playing", reduce: () => ({}) } },
},
volume: {
Audible: { Mute: { target: "Muted", reduce: () => ({}) } },
Muted: { Unmute: { target: "Audible", reduce: () => ({}) } },
},
},
{ Stop: { target: "Stopped", reduce: () => ({}) } },
)Region configuration is explicit state data. A transition entering Active must supply values for
playback and volume; there are no hidden initial child states. Tagged-union fields that are not
listed in regions(...) remain ordinary inert data. History is therefore modeled by copying a slot
value into a normal field on exit and restoring it in a later entry reducer.
Events are selected innermost first. A child transition, stay, or ignore suppresses the parent handler for that event. Otherwise the parent handler is the fallback. When parallel siblings handle one event, all selected reducers read the same pre-event parent snapshot and their slot updates are committed atomically. Region targets stay within their own slot.
A region child can use machine.region.invoke(...), own an after timer, or be final. When every
declared slot is final, the parent selects onComplete after committing the completing macrostep.
Without onComplete, the completed region configuration remains stable.
See the compile-checked player, editor, and importer definitions in
packages/core/examples/Statecharts.ts and the permanent inference contract in
packages/core/tests/Statechart.types.ts.
Studio renders each region slot as a labeled boundary and derives its active child directly from the schema-encoded parent state. When one event selects transitions in several parallel slots, history keeps them together as one macrostep and highlights every traversed edge. Invoked nodes expose their work kind, lanes, concurrency, retry policy, and safe outcome-Schema metadata; timer lifecycles and stale outcomes remain visible as semantic history rows. The same metadata is available in the versioned Studio protocol and raw JSON view.