Guide

Getting Started

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.

Writing a Motive application

Declarative model

States and events are TypeScript objects backed by Effect Schema. State definitions describe the system's data and topology, the events it can respond to, the transitions it can take, and the work that begins or ends as states become active or inactive. Together, they form a typed Statechart: the model of the system.

Effectful runtime

A StatechartEngine supplies the execution and persistence model. The chart's toLayer method binds its required runtime implementations and registers the chart with the engine through an Effect Layer. The chart's typed client starts an addressed instance, sends it events, and reads its committed state.

Example:Start with the smallest valid statechart: one atomic state. Add topology and events, turn the chart into a Layer, provide a StatechartEngine, then use the chart's client to start an addressed instance.

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.