How it works
One principle shapes the entire API. Internalise it and the rest follows.
The one idea: the engine computes, adapters perform
The engine never performs a side effect. It does not read the clock, sleep, start a goroutine, make a network call, or touch the filesystem. Given a machine and a sequence of events, it computes the next state — and nothing else.
Real machines clearly do things: they wait for a timeout, call a service, spawn a child. fate handles this by treating those as data, not actions. When a state wants to wait, the engine records a pending timer. When a state wants to call a service, it records a pending invocation. It exposes that intent and waits to be told the outcome:
state enters "loading"
│
├─ engine: record pending invocation {id, src: "charge-card", input}
│
adapter reads PendingInvocations(), runs the work, and reports back:
│
└─ engine: ResolveInvocation(id, result) → fires the OnDone transitionThe component that reads those pending effects and performs them is an adapter. Different adapters perform the same intent in different worlds:
- a real-time adapter arms an OS timer and calls an HTTP endpoint;
- a Temporal adapter maps the timer to
workflow.NewTimerand the invocation toworkflow.ExecuteActivity, so the work is durable and survives restarts; - a test drives both by hand, with no clock and no I/O at all.
The engine is identical in every case. That is why the same machine can run in a unit test in microseconds and in a Temporal workflow for three weeks, and produce the same transitions.
Machine and Actor
The library separates the definition of a machine from a running instance:
- A
Machineis immutable and validated once at construction. It is safe to share across goroutines and to back many running instances. It holds no runtime state. - An
Actoris one running instance. It holds the active configuration and the context, processes events, and serialises to and from JSON.
This split is what makes the engine cheap to embed: build the machine once at start-up, spin up an actor per workflow or per request.
The active configuration
Because of hierarchy and parallelism, "the current state" is not a single value. At any moment a machine has an active configuration: the set of states active together. fate represents it as a StateValue — a bare string for an atomic state, or a map of region name to sub-value for a compound or parallel state.
Path() renders the leaves as dotted paths, joined by | across parallel regions:
review.bpkb.checking | review.head_vdWhy determinism matters here
Keeping side effects out of the engine has a second payoff: determinism. The same machine and the same events always produce the same active configuration and the same serialised snapshot, byte for byte. Nothing inside the engine can vary between runs, because nothing inside it reads the outside world.
Durable execution engines such as Temporal require this: they re-run workflow code to rebuild state after a failure, and that re-run must reproduce the original decisions exactly. A machine that called time.Now() in a guard would break on replay. fate cannot, because the guard has no way to call it.
The flow, end to end
┌─────────────┐ Send(event) ┌──────────────────┐
│ your code │ ───────────────▶ │ Actor (engine) │
└─────────────┘ │ pure: computes │
▲ │ next state + │
│ Snapshot / Subscribe │ pending effects │
│ └────────┬─────────┘
│ │ PendingTimers / PendingInvocations
│ ▼
┌─────┴───────┐ FireTimer / ┌──────────────────┐
│ adapter │ ResolveInvocation│ the real world │
│ (you/Temporal) ◀────────────────│ clocks, I/O, │
└─────────────┘ performs effects │ child actors │
└──────────────────┘See it live
This compute-vs-perform model is exactly what the studio makes visible: open a machine, send events, and watch the active configuration light up while a Pending effects panel lets you fire timers and resolve invocations by hand. The picture itself is generated from the machine's structure — see how the chart is generated.
Next
- Concepts — statecharts, the configuration,
MachinevsActor. - The studio — render and drive any machine in the browser.
- Examples — runnable machines that show each feature.
- Deep dives on GitHub: effects & adapters, persistence & determinism, Temporal.