Skip to main content

harel

Durable, distributed statecharts for Python.

CI License: Apache 2.0 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

Six runnable examples live in examples/ — an order lifecycle, a webhook-driven payment with a worker, a NiceGUI form, a shop whose orders commit in its own transaction — each explained, with its sequence diagrams and why it runs the way it does, in the examples section of the docs.

Documentation

Read the docs online at acasadom.github.io/harel — a step-by-step tutorial (17 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.


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, computed select branches, parametrized fragments, imports, and black-box invoke of sub-machines (including data-parallel fan-out).
  • A pure, effects-based engine: start/process are generators that describe effects (run an action, emit an event, spawn regions) and mutate a serializable Execution. 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.base and 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.8.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for harel 0.8.0
File Size Uploaded
harel-0.8.0.tar.gz 1.7 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for harel 0.8.0
File Interpreter ABI Platform
harel-0.8.0-py3-none-any.whl Python 3 none any Details

Total release size: 2.0 MB

Release files / harel-0.8.0.tar.gz

Download URL harel-0.8.0.tar.gz
Size 1.7 MB
Tags Source
SHA-256 checksum
How to use checksums
b2d50cd662820bffc9ff5fa18c539d3102c32aba26d9afb0d87d587352a63493
BLAKE2b-256 checksum
How to use checksums
cf34de8cc3e589fcf1927e0c6bf838f181d606048d0f5c938eb1becf3769ee03
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

Release files / harel-0.8.0-py3-none-any.whl

Download URL harel-0.8.0-py3-none-any.whl
Size 306.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
acbe929fe30ce02d3c09138f16fe2e36e6ba7827d5bdf832b7b04881a261c789
BLAKE2b-256 checksum
How to use checksums
6f6e660f98fd786d493e9020c94de099c9e74cde9d3504496388a2a662478166
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

Release history Release notifications | RSS feed

0.9.0

2 release files

This release

0.8.0 This release

2 release files

0.7.1

2 release files

0.7.0

2 release files

0.6.1

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page