Motive is a language for modeling durable, stateful systems, built on TypeScript and Effect.
Terminology
- Statechart
- A declarative model of a stateful system, organized as states and transitions. The formalism comes from Harel's statecharts; Motive uses W3C SCXML as its semantic baseline.
- Instance
- One independently running system described by a statechart, with its own identity and current configuration.
- State
- A meaningful situation that may be active.
- Configuration
- The set of states active in an instance at a particular moment.
- Event
- A typed occurrence an instance may observe.
- Transition
- A declared response that may change the active configuration or act within it.
Applications
Motive is ideal for applications that must coordinate state, time, concurrency, interruption, failure, recovery, and external work.
- Interactive
User interfaces
Make interaction modes, navigation, and asynchronous work explicit.
- Orchestrated
Workflows
Carry long-running processes through delays, retries, restarts, and human decisions.
- Autonomous
Agents
Coordinate tool use, delegation, interruption, and supervised lifecycles.
- Resilient
Distributed systems
Coordinate services while preserving causation, failure, and recovery.
- Reactive
Automation & control
Connect time, external work, and feedback to durable state.
- Fractal
Recursive systems
Build systems whose parts may contain or supervise systems like themselves.
- Evolving
Long-lived systems
Version charts and migrate running instances as behavior and data change.
- Observable
Operational systems
Trace causation, reconstruct incidents, and explain how each instance reached its current state.
1 · Atomic state
The smallest valid statechart is a single atomic state.
Lamp extends State.Atomic, giving the state a name and no children. Statechart.make(Lamp) assembles it as a complete chart; once started, Lamp is the only active state.
import { State, Statechart } from "@motive/motive";
class Lamp extends State.Atomic<Lamp>()("Lamp") {}
export const LampChart = Statechart.make(Lamp);
2 · Compound state
A compound state contains other states.
Lamp now extends State.Compound and declares Off and Dimmed with States.make. The first child is initial by default, so a new instance begins with Lamp and its child Off active.
import { State, Statechart, States } from "@motive/motive";
class Lamp extends State.Compound<Lamp>()("Lamp") {
static states = States.make(() => [Off, Dimmed]);
}
class Off extends State.Atomic<Off>()("Off") {}
class Dimmed extends State.Atomic<Dimmed>()("Dimmed") {}
export const LampChart = Statechart.make(Lamp);
3 · Event transition
Events trigger transitions between states.
SwitchOn is an event defined with a tagged Effect Schema. Off declares on(SwitchOn, Dimmed), so receiving SwitchOn moves the lamp from Off to Dimmed.
import { State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";
const SwitchOn = Schema.TaggedStruct("SwitchOn", {});
class Lamp extends State.Compound<Lamp>()("Lamp") {
static states = States.make(() => [Off, Dimmed]);
}
class Off extends State.Atomic<Off>()("Off") {
static transitions = Transitions.make(this, ({ on }) => [
on(SwitchOn, Dimmed),
]);
}
class Dimmed extends State.Atomic<Dimmed>()("Dimmed") {}
export const LampChart = Statechart.make(Lamp);
4 · Return transition
Cycles are allowed.
Dimmed declares on(SwitchOff, Off), completing a cycle with the transition from Off to Dimmed. A statechart does not have to move steadily forward; it can revisit states whenever the model allows.
import { State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";
const SwitchOn = Schema.TaggedStruct("SwitchOn", {});
const SwitchOff = Schema.TaggedStruct("SwitchOff", {});
class Lamp extends State.Compound<Lamp>()("Lamp") {
static states = States.make(() => [Off, Dimmed]);
}
class Off extends State.Atomic<Off>()("Off") {
static transitions = Transitions.make(this, ({ on }) => [
on(SwitchOn, Dimmed),
]);
}
class Dimmed extends State.Atomic<Dimmed>()("Dimmed") {
static transitions = Transitions.make(this, ({ on }) => [on(SwitchOff, Off)]);
}
export const LampChart = Statechart.make(Lamp);
5 · New state
States model the domain.
Bright adds a distinction the chart could not express before. Dimmed declares on(Brighten, Bright), so brightness can change only while the lamp is already on.
import { State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";
const SwitchOn = Schema.TaggedStruct("SwitchOn", {});
const SwitchOff = Schema.TaggedStruct("SwitchOff", {});
const Brighten = Schema.TaggedStruct("Brighten", {});
class Lamp extends State.Compound<Lamp>()("Lamp") {
static states = States.make(() => [Off, Dimmed, Bright]);
}
class Off extends State.Atomic<Off>()("Off") {
static transitions = Transitions.make(this, ({ on }) => [
on(SwitchOn, Dimmed),
]);
}
class Dimmed extends State.Atomic<Dimmed>()("Dimmed") {
static transitions = Transitions.make(this, ({ on }) => [
on(SwitchOff, Off),
on(Brighten, Bright),
]);
}
class Bright extends State.Atomic<Bright>()("Bright") {}
export const LampChart = Statechart.make(Lamp);
6 · Shared behavior
Repetition is a modeling clue.
Bright declares on(Dim, Dimmed) and repeats on(SwitchOff, Off). The repetition is correct in this flat model, but it reveals that Dimmed and Bright share a fact the topology cannot yet name.
import { State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";
const SwitchOn = Schema.TaggedStruct("SwitchOn", {});
const SwitchOff = Schema.TaggedStruct("SwitchOff", {});
const Brighten = Schema.TaggedStruct("Brighten", {});
const Dim = Schema.TaggedStruct("Dim", {});
class Lamp extends State.Compound<Lamp>()("Lamp") {
static states = States.make(() => [Off, Dimmed, Bright]);
}
class Off extends State.Atomic<Off>()("Off") {
static transitions = Transitions.make(this, ({ on }) => [
on(SwitchOn, Dimmed),
]);
}
class Dimmed extends State.Atomic<Dimmed>()("Dimmed") {
static transitions = Transitions.make(this, ({ on }) => [
on(SwitchOff, Off),
on(Brighten, Bright),
]);
}
class Bright extends State.Atomic<Bright>()("Bright") {
static transitions = Transitions.make(this, ({ on }) => [
on(Dim, Dimmed),
on(SwitchOff, Off),
]);
}
export const LampChart = Statechart.make(Lamp);
7 · Hierarchy
Hierarchy models what states share.
A new compound state, On, names that shared fact and groups Dimmed and Bright as its children. When Dimmed is active, its ancestors—On and Lamp—are active too. Entering On activates its initial child, Dimmed.
import { State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";
const SwitchOn = Schema.TaggedStruct("SwitchOn", {});
const SwitchOff = Schema.TaggedStruct("SwitchOff", {});
const Brighten = Schema.TaggedStruct("Brighten", {});
const Dim = Schema.TaggedStruct("Dim", {});
class Lamp extends State.Compound<Lamp>()("Lamp") {
static states = States.make(() => [Off, On]);
}
class Off extends State.Atomic<Off>()("Off") {
static transitions = Transitions.make(this, ({ on }) => [on(SwitchOn, On)]);
}
class On extends State.Compound<On>()("On") {
static states = States.make(() => [Dimmed, Bright]);
}
class Dimmed extends State.Atomic<Dimmed>()("Dimmed") {
static transitions = Transitions.make(this, ({ on }) => [
on(SwitchOff, Off),
on(Brighten, Bright),
]);
}
class Bright extends State.Atomic<Bright>()("Bright") {
static transitions = Transitions.make(this, ({ on }) => [
on(Dim, Dimmed),
on(SwitchOff, Off),
]);
}
export const LampChart = Statechart.make(Lamp);
8 · Nearest owner
Shared behavior belongs to its nearest owner.
SwitchOff moves from both brightness states to On. Because On remains active throughout either child configuration, one transition replaces both copies and stays available from Dimmed and Bright.
import { State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";
const SwitchOn = Schema.TaggedStruct("SwitchOn", {});
const SwitchOff = Schema.TaggedStruct("SwitchOff", {});
const Brighten = Schema.TaggedStruct("Brighten", {});
const Dim = Schema.TaggedStruct("Dim", {});
class Lamp extends State.Compound<Lamp>()("Lamp") {
static states = States.make(() => [Off, On]);
}
class Off extends State.Atomic<Off>()("Off") {
static transitions = Transitions.make(this, ({ on }) => [on(SwitchOn, On)]);
}
class On extends State.Compound<On>()("On") {
static states = States.make(() => [Dimmed, Bright]);
static transitions = Transitions.make(this, ({ on }) => [on(SwitchOff, Off)]);
}
class Dimmed extends State.Atomic<Dimmed>()("Dimmed") {
static transitions = Transitions.make(this, ({ on }) => [
on(Brighten, Bright),
]);
}
class Bright extends State.Atomic<Bright>()("Bright") {
static transitions = Transitions.make(this, ({ on }) => [on(Dim, Dimmed)]);
}
export const LampChart = Statechart.make(Lamp);
9 · Runtime layer
A Layer supplies the runtime.
LampChart.toLayer() builds the Effect runtime layer for this declarative chart. Lamp requires no activity, timer, emit, retry, or Resource implementations, so there is no placeholder configuration to pass.
import { State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";
const SwitchOn = Schema.TaggedStruct("SwitchOn", {});
const SwitchOff = Schema.TaggedStruct("SwitchOff", {});
const Brighten = Schema.TaggedStruct("Brighten", {});
const Dim = Schema.TaggedStruct("Dim", {});
class Lamp extends State.Compound<Lamp>()("Lamp") {
static states = States.make(() => [Off, On]);
}
class Off extends State.Atomic<Off>()("Off") {
static transitions = Transitions.make(this, ({ on }) => [on(SwitchOn, On)]);
}
class On extends State.Compound<On>()("On") {
static states = States.make(() => [Dimmed, Bright]);
static transitions = Transitions.make(this, ({ on }) => [on(SwitchOff, Off)]);
}
class Dimmed extends State.Atomic<Dimmed>()("Dimmed") {
static transitions = Transitions.make(this, ({ on }) => [
on(Brighten, Bright),
]);
}
class Bright extends State.Atomic<Bright>()("Bright") {
static transitions = Transitions.make(this, ({ on }) => [on(Dim, Dimmed)]);
}
export const LampChart = Statechart.make(Lamp);
export const LampLive = LampChart.toLayer();
10 · Addressed client
A client selects one instance.
Statechart.id("desk-lamp") creates the durable lampId. LampChart.client yields a typed client factory, and makeLamp(lampId) binds it to that one running lamp.
import { State, Statechart, States, Transitions } from "@motive/motive";
import { Effect, Schema } from "effect";
const SwitchOn = Schema.TaggedStruct("SwitchOn", {});
const SwitchOff = Schema.TaggedStruct("SwitchOff", {});
const Brighten = Schema.TaggedStruct("Brighten", {});
const Dim = Schema.TaggedStruct("Dim", {});
class Lamp extends State.Compound<Lamp>()("Lamp") {
static states = States.make(() => [Off, On]);
}
class Off extends State.Atomic<Off>()("Off") {
static transitions = Transitions.make(this, ({ on }) => [on(SwitchOn, On)]);
}
class On extends State.Compound<On>()("On") {
static states = States.make(() => [Dimmed, Bright]);
static transitions = Transitions.make(this, ({ on }) => [on(SwitchOff, Off)]);
}
class Dimmed extends State.Atomic<Dimmed>()("Dimmed") {
static transitions = Transitions.make(this, ({ on }) => [
on(Brighten, Bright),
]);
}
class Bright extends State.Atomic<Bright>()("Bright") {
static transitions = Transitions.make(this, ({ on }) => [on(Dim, Dimmed)]);
}
export const LampChart = Statechart.make(Lamp);
export const LampLive = LampChart.toLayer();
const lampId = Statechart.id("desk-lamp");
export const lampClient = Effect.gen(function* () {
const makeLamp = yield* LampChart.client;
return makeLamp(lampId);
});
11 · Commands and queries
Commands change; queries observe.
start and send.SwitchOn are commands sent to the addressed instance. snapshot reads committed state before and after the transition, and Effect.log makes those query results visible.
import { State, Statechart, States, Transitions } from "@motive/motive";
import { Effect, Schema } from "effect";
const SwitchOn = Schema.TaggedStruct("SwitchOn", {});
const SwitchOff = Schema.TaggedStruct("SwitchOff", {});
const Brighten = Schema.TaggedStruct("Brighten", {});
const Dim = Schema.TaggedStruct("Dim", {});
class Lamp extends State.Compound<Lamp>()("Lamp") {
static states = States.make(() => [Off, On]);
}
class Off extends State.Atomic<Off>()("Off") {
static transitions = Transitions.make(this, ({ on }) => [on(SwitchOn, On)]);
}
class On extends State.Compound<On>()("On") {
static states = States.make(() => [Dimmed, Bright]);
static transitions = Transitions.make(this, ({ on }) => [on(SwitchOff, Off)]);
}
class Dimmed extends State.Atomic<Dimmed>()("Dimmed") {
static transitions = Transitions.make(this, ({ on }) => [
on(Brighten, Bright),
]);
}
class Bright extends State.Atomic<Bright>()("Bright") {
static transitions = Transitions.make(this, ({ on }) => [on(Dim, Dimmed)]);
}
export const LampChart = Statechart.make(Lamp);
export const LampLive = LampChart.toLayer();
const lampId = Statechart.id("desk-lamp");
const lampClient = Effect.gen(function* () {
const makeLamp = yield* LampChart.client;
return makeLamp(lampId);
});
export const program = Effect.gen(function* () {
const lamp = yield* lampClient;
yield* lamp.start();
const before = yield* lamp.snapshot;
yield* Effect.log("Before SwitchOn", before.configuration);
yield* lamp.send.SwitchOn();
const after = yield* lamp.snapshot;
yield* Effect.log("After SwitchOn", after.configuration);
});
12 · Application boundary
Run the completed program.
Layer.provideMerge wires the in-memory engine into LampLive and keeps both available to the program. Effect.provide supplies the completed runtime, Effect.scoped owns its lifetime, and NodeRuntime.runMain starts the Node.js process.
import { NodeRuntime } from "@effect/platform-node";
import {
State,
Statechart,
StatechartEngine,
States,
Transitions,
} from "@motive/motive";
import { Effect, Layer, Schema } from "effect";
const SwitchOn = Schema.TaggedStruct("SwitchOn", {});
const SwitchOff = Schema.TaggedStruct("SwitchOff", {});
const Brighten = Schema.TaggedStruct("Brighten", {});
const Dim = Schema.TaggedStruct("Dim", {});
class Lamp extends State.Compound<Lamp>()("Lamp") {
static states = States.make(() => [Off, On]);
}
class Off extends State.Atomic<Off>()("Off") {
static transitions = Transitions.make(this, ({ on }) => [on(SwitchOn, On)]);
}
class On extends State.Compound<On>()("On") {
static states = States.make(() => [Dimmed, Bright]);
static transitions = Transitions.make(this, ({ on }) => [on(SwitchOff, Off)]);
}
class Dimmed extends State.Atomic<Dimmed>()("Dimmed") {
static transitions = Transitions.make(this, ({ on }) => [
on(Brighten, Bright),
]);
}
class Bright extends State.Atomic<Bright>()("Bright") {
static transitions = Transitions.make(this, ({ on }) => [on(Dim, Dimmed)]);
}
export const LampChart = Statechart.make(Lamp);
const LampLive = LampChart.toLayer();
const lampId = Statechart.id("desk-lamp");
const lampClient = Effect.gen(function* () {
const makeLamp = yield* LampChart.client;
return makeLamp(lampId);
});
const program = Effect.gen(function* () {
const lamp = yield* lampClient;
yield* lamp.start();
const before = yield* lamp.snapshot;
yield* Effect.log("Before SwitchOn", before.configuration);
yield* lamp.send.SwitchOn();
const after = yield* lamp.snapshot;
yield* Effect.log("After SwitchOn", after.configuration);
});
program.pipe(
Effect.provide(
LampLive.pipe(Layer.provideMerge(StatechartEngine.layerMemory)),
),
Effect.scoped,
NodeRuntime.runMain,
);
Continue with State Modeling
Next, Statecharts & Instances shows how one statechart can describe many independently running instances. From there, State Modeling continues through atomic, compound, and parallel states.