Skip to main content

Durable, distributed statecharts for Python

Project description

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

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.


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.
  • 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

harel-0.1.2.tar.gz (1.5 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

harel-0.1.2-py3-none-any.whl (227.7 kB view details)

Uploaded Python 3

File details

Details for the file harel-0.1.2.tar.gz.

File metadata

  • Download URL: harel-0.1.2.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

Hashes for harel-0.1.2.tar.gz
Algorithm Hash digest
SHA256 33c6ac2489273474c16d54eb2f6c50e256643667d45fd5c950035e8d1904ab61
MD5 e96345f4b02d4fed62cfb91fc8ae9711
BLAKE2b-256 a2b95fc2ec453b38db96b56169b843877ea96d5205aa153711774a254a94878e

See more details on using hashes here.

Provenance

The following attestation bundles were made for harel-0.1.2.tar.gz:

Publisher: publish.yml on acasadom/harel

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file harel-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: harel-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 227.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for harel-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 f2cf948713a1e5be68a47b16f9dd4e0cd26501e4699adbf6f77d70237246c7bf
MD5 8933a06d2423ca77e77574a7f22bdd56
BLAKE2b-256 560e40a2edc2e5cddb7522a067ed94806a82bf5bf1c69d1235f7ad5ad4b6b7ee

See more details on using hashes here.

Provenance

The following attestation bundles were made for harel-0.1.2-py3-none-any.whl:

Publisher: publish.yml on acasadom/harel

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page