harel
Durable, distributed statecharts for Python.
A hierarchical statechart engine for Python — with the durability and distribution of a workflow engine, but the right model underneath.
You author a machine in a small textual DSL (.stm), it compiles to an immutable
Definition, and a pure, effects-based engine runs it against a serializable
Execution that can be persisted, distributed across workers, and survive crashes.
Why this exists
In ten years across different companies I kept hitting the same problem: a thing
that was really a hierarchical state machine — an order, a subscription, a device, a
claim, a deployment — got called a "pipeline", a "workflow", or just "a graph". So it
got built with the tool that matched the name instead of the shape: a DAG runner, a
task queue, a pile of if status == ... branches.
The result was always the same: states implied but never named, transitions scattered across the codebase, illegal states reachable, and "creative" retry/cancel logic that was hell to debug, follow, or extend.
A statechart is the correct model for that shape — hierarchy, orthogonal regions, guarded transitions, explicit terminal verdicts — and it has been since Harel, 1987. What was missing in Python was a statechart engine that is also durable and distributed:
- sismic models statecharts well but is in-memory only.
- Temporal / DBOS give you durable execution, but model work as imperative code, not as a declarative statechart.
- XState is the gold standard for statecharts — but it's JavaScript.
harel sits in that gap: the statechart as the model, durability and distribution as
the runtime. See the comparison below.
Install
pip install harel # core: DSL + engine + in-memory/sqlite durability
# optional backends, pick what you need:
pip install "harel[redis]" # Redis store + transport
pip install "harel[postgres]" # Postgres store + transport
pip install "harel[mongo]" # MongoDB store + transport
pip install "harel[libsql]" # libSQL store + transport — EXPERIMENTAL (local file tested; Turso/sqld path needs a Turso account to validate)
pip install "harel[dynamodb]" # DynamoDB store (pairs with sqs for an all-AWS stack)
pip install "harel[sqs]" # AWS SQS FIFO transport
pip install "harel[lsp]" # DSL language server (editor tooling)
Requires Python 3.11+.
Quickstart
Scaffold a starter machine that validates and runs out of the box — zero to a working state machine in one command:
harel new approval.stm
harel run approval.stm -e Submit -e Approve # (start) -> Draft -> Review -> Approved
Or author one yourself — events are declared up front, transitions live inside the state they leave, and terminals declare their verdict:
event PlaceOrder {}
event PaymentAuthorized {}
event CancelOrder {}
event Delivered {}
machine order {
initial Cart
state Cart {}
state AwaitingPayment {}
state Paid {}
final Delivered success
final Cancelled cancelled
from Cart to AwaitingPayment on PlaceOrder
from AwaitingPayment to Paid on PaymentAuthorized
from AwaitingPayment to Cancelled on CancelOrder
from Paid to Delivered on Delivered
}
Run it through the headless durable runner (here over an in-memory store):
from harel import definition_from_dsl, DurableRunner, DictStore, Event
SOURCE = """
event PlaceOrder {}
event PaymentAuthorized {}
event CancelOrder {}
event Delivered {}
machine order {
initial Cart
state Cart {}
state AwaitingPayment {}
state Paid {}
final Delivered success
final Cancelled cancelled
from Cart to AwaitingPayment on PlaceOrder
from AwaitingPayment to Paid on PaymentAuthorized
from AwaitingPayment to Cancelled on CancelOrder
from Paid to Delivered on Delivered
}
"""
defn = definition_from_dsl(SOURCE, "order") # compile the DSL
runner = DurableRunner(DictStore(), {defn.id: defn}) # swap DictStore for Sqlite/Redis/Postgres
exe = runner.create(defn.id)
for kind in ["PlaceOrder", "PaymentAuthorized", "Delivered"]:
exe = runner.process(exe.id, Event(kind=kind))
print(f"{kind} -> {exe.active_path}")
print(exe.status.name, exe.outcome)
PlaceOrder -> AwaitingPayment
PaymentAuthorized -> Paid
Delivered -> Delivered
DONE success
A complete, runnable example (nested states, a selector-driven retry, actions) lives in
examples/place_order/ — run it with
uv run python -m examples.place_order.run.
Documentation
Read the docs online at acasadom.github.io/harel — a
step-by-step tutorial (14 stages that grow one example from a turnstile to durable,
distributed submachines) plus operations and reference guides. The source lives under
docs/; build the HTML locally with make docs. Every code example in the docs is
executed in CI, so it stays in sync with the engine.
- Tutorial — start here; the model, one step at a time.
- Architecture — how harel works inside: the pure engine, the effect protocol, the single atomic checkpoint, in-memory vs. distributed (with diagrams).
- Reference: DSL · CLI · Public API · Visualization.
- Operations: durability · distribution · control plane · monitor TUI · remote actions / FaaS.
Where it fits
| harel | sismic | transitions | XState | Temporal / DBOS | |
|---|---|---|---|---|---|
| Hierarchical statechart | ✅ | ✅ | partial | ✅ | ❌ |
| Orthogonal regions | ✅ | ✅ | ❌ | ✅ | ❌ |
| Declarative model | ✅ | ✅ | ✅ | ✅ | ❌ (code) |
| Durable / crash-safe | ✅ | ❌ | ❌ | partial | ✅ |
| Distributed workers | ✅ | ❌ | ❌ | ❌ | ✅ |
| Language | Python | Python | Python | JS/TS | Go/Java/… |
Use a statechart when the domain is a machine of named states with hierarchy, guarded transitions and explicit terminal verdicts. If your domain is genuinely "run these steps in order with retries", a workflow engine is the better fit — this is not trying to replace one.
What's in the box
- A textual DSL (
.stm, parsed with lark) that reads like a spec: nested composite states, orthogonal regions, guarded transitions, named guards, computedselectbranches, parametrizedfragments,imports, and black-boxinvokeof sub-machines (including data-parallel fan-out). - A pure, effects-based engine:
start/processare generators that describe effects (run an action, emit an event, spawn regions) and mutate a serializableExecution. No IO, no user code inside the engine — which makes runs deterministic and trivially testable. - Durable & distributed execution: optimistic-concurrency (CAS) checkpointing, a
transactional outbox, event dedupe, durable timers, and a control plane
(
cancel/suspend/resume/terminate). Stores: in-memory, SQLite, Redis, Postgres, rqlite, MongoDB, DynamoDB (+ libSQL/Turso, experimental). Transports: in-memory, SQLite, Redis, Postgres, rqlite, MongoDB, SQS (+ libSQL/Turso, experimental) — mix freely. - Sync or async, your choice of execution model: async runners and worker for asyncio code,
and sync runners that run either on a shared background event loop or inline in the caller's
own thread (
execution="inline") — on the caller's database connection and inside its transaction. The same logic and the same commit under each. - Your own backend: a store or transport written outside harel builds on
harel.engine.store.base/harel.engine.transport.baseand checks itself against the same contract tests harel runs on its own (harel.testing). - Static validation (
validate): unreachable states, non-deterministic transitions, unresolved selector targets, missing terminal verdicts, timeout shape — surface-independent, run it before you execute. - Editor tooling: a DSL language server (diagnostics, hover, go-to-definition, completion across imports) and a VSCode extension with a live Mermaid statechart preview.
- Visualization: render any machine to PlantUML or Mermaid.
Design principle: it's a statechart, not a job engine
The engine schedules; the model decides. Retry/backoff is a composite with a timeout
and a selector — not an engine feature. Cancellation is a modelled on Cancel transition when
the machine wants to own its cleanup. There is no hidden "default to success/failed" — a
validation rule forces you to declare the terminal verdict where it's consumed, instead of
the engine guessing. Policy stays in the model; the engine stays small.
Status
Beta. The core engine, DSL, durability/distribution layers, the monitor TUI, remote action
execution on FaaS (AWS Lambda / HTTP functions), and editor tooling are all in place and covered
by an extensive test suite (run uv run pytest); every code example in the docs is executed in CI.
Roadmap: validating the libSQL/Turso path against a real account (today experimental); fault-injection testing of the distributed path under crash/lease-expiry; opt-in bounded retry for transient action errors.
Development
uv sync # create/refresh the venv (Python 3.13)
uv run pytest # run the suite
make lint # ruff + .stm formatter check
make type-check # mypy
The name
Named after David Harel, who introduced statecharts in his 1987 paper Statecharts: A Visual Formalism for Complex Systems (PDF · DOI), and later recounted how it came to be in Statecharts in the Making: A Personal Account (2007). This engine makes that formalism durable and distributed, in his honour.
License
Apache License 2.0 © Alberto Casado
Metadata
Release files for harel 0.6.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| harel-0.6.1.tar.gz | 1.6 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| harel-0.6.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.9 MB
Release files / harel-0.6.1.tar.gz
| Download URL | harel-0.6.1.tar.gz |
|---|---|
| Size | 1.6 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
01e4583b1a5f163d2f20bef27768146e00588c287fb3d7412540b0ec73531303
|
|
BLAKE2b-256 checksum How to use checksums |
fd07fabc042923d3b0c59a5442d15fe7291c27839001d8e853111e5f22bf77b7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 4, 2026.
Transparency logRelease files / harel-0.6.1-py3-none-any.whl
| Download URL | harel-0.6.1-py3-none-any.whl |
|---|---|
| Size | 300.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4dea7d36647f90f30a10d3932ef17f95e49daab9c69c1594cc8431caa732c635
|
|
BLAKE2b-256 checksum How to use checksums |
b91a339e3923b774bfd875fad4674cedb21a2abda5f584f3e43c224eccedb3c0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 4, 2026.
Transparency log