1. Your first machine¶
We start with the smallest interesting state machine there is: a turnstile. It has two
states — Locked and Unlocked — and reacts to two events: inserting a Coin unlocks it;
a Push (someone walking through) locks it again.
This is the hello world of state machines. By the end of this page you will have authored a machine in the DSL, run it, and driven it with events.
Install¶
pip install harel
harel needs Python 3.11+. The core install has everything in this tutorial; optional
backends (Redis, Postgres, …) and the editor tooling are extras you add later.
In a hurry?
harel new mymachine.stmwrites a commented starter machine that already validates and runs — then edit it as you read on.
Run it¶
Here is the canonical way to run any machine in this tutorial: compile the DSL into a
Definition, hand it to a DurableRunner backed by a store, create an execution, then feed
it events with process. We use the in-memory DictStore here; swapping in SQLite, Redis, or
Postgres is a one-line change covered under durability.
from harel import definition_from_dsl, DurableRunner, DictStore, Event
SOURCE = """
machine turnstile {
initial Locked
state Locked {}
state Unlocked {}
from Locked to Unlocked on Coin
from Unlocked to Locked on Push
}
"""
defn = definition_from_dsl(SOURCE, "turnstile") # compile the DSL
runner = DurableRunner(DictStore(), {defn.id: defn})
exe = runner.create(defn.id) # a fresh execution
print("start ->", exe.active_path)
for kind in ["Coin", "Push", "Coin"]:
exe = runner.process(exe.id, Event(kind=kind))
print(f"{kind:<8} -> {exe.active_path} ({exe.status.name})")
Running it prints:
start -> Locked
Coin -> Unlocked (RUNNING)
Push -> Locked (RUNNING)
Coin -> Unlocked (RUNNING)
What just happened¶
definition_from_dsl(SOURCE, "turnstile")parses and compiles the text into an immutableDefinition. Thenameargument selects the machine; it is only required when a file declares more than one.DurableRunner(store, {defn.id: defn})is the headless runner. It takes a store (where executions are persisted) and a registry mapping each definition’sidto itsDefinition.runner.create(defn.id)starts a newExecutionand returns it.exe.active_pathis the state it is currently in —Locked, theinitialstate.runner.process(exe.id, Event(kind=...))delivers one event and returns the updated execution. An event is just akind(its name) plus optionaldata— more on data in payloads.exe.statusis the lifecycle status. It isRUNNINGthe whole time here, because the turnstile never finishes — it just toggles forever.
A machine that never ends is fine, but most useful machines reach a conclusion: an order is delivered or cancelled, a review is approved or rejected. That conclusion — a terminal state and the verdict it carries — is the subject of step 3. First, in step 2, we make the states actually do something.