Durable, distributed statecharts for Python
Project description
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. - 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
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file harel-0.1.1.tar.gz.
File metadata
- Download URL: harel-0.1.1.tar.gz
- Upload date:
- Size: 1.5 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ef572092da71ad4cc896ecd76abde1aef66a788c5ea858967bb7b5178677a809
|
|
| MD5 |
b9fdfe430e5617ede0202312367d6234
|
|
| BLAKE2b-256 |
1aa3571ac59b4bb5c64efd933aa70f97f814d0ad63e579016c6d546616bcefb6
|
Provenance
The following attestation bundles were made for harel-0.1.1.tar.gz:
Publisher:
publish.yml on acasadom/harel
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
harel-0.1.1.tar.gz -
Subject digest:
ef572092da71ad4cc896ecd76abde1aef66a788c5ea858967bb7b5178677a809 - Sigstore transparency entry: 1852621661
- Sigstore integration time:
-
Permalink:
acasadom/harel@cd5367230fb6fbe30e9a705d015d9931af810e34 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/acasadom
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@cd5367230fb6fbe30e9a705d015d9931af810e34 -
Trigger Event:
release
-
Statement type:
File details
Details for the file harel-0.1.1-py3-none-any.whl.
File metadata
- Download URL: harel-0.1.1-py3-none-any.whl
- Upload date:
- Size: 222.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cec5a23b21a80e8ee5d35cc69c622f0307a91b1900bfb4fa5e2a4c7685a39b73
|
|
| MD5 |
c99becb3a9b0d0f6f92b31d476a12515
|
|
| BLAKE2b-256 |
cf32772990be5fbb62933e845e9f6eba7bbbb6bea81709587c76fda71fe42ed2
|
Provenance
The following attestation bundles were made for harel-0.1.1-py3-none-any.whl:
Publisher:
publish.yml on acasadom/harel
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
harel-0.1.1-py3-none-any.whl -
Subject digest:
cec5a23b21a80e8ee5d35cc69c622f0307a91b1900bfb4fa5e2a4c7685a39b73 - Sigstore transparency entry: 1852621738
- Sigstore integration time:
-
Permalink:
acasadom/harel@cd5367230fb6fbe30e9a705d015d9931af810e34 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/acasadom
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@cd5367230fb6fbe30e9a705d015d9931af810e34 -
Trigger Event:
release
-
Statement type: