Public API¶
Everything you need is re-exported from the top-level harel package, so callers stay
decoupled from the internal module layout:
from harel import (
definition_from_dsl, definition_from_dsl_file, DslError, # compile the DSL
machine_version, definition_registry, # machine versions (+ harel.published)
validate, validate_or_raise, Issue, ValidationError, # static checks
render, # PlantUML (mermaid: harel.viz.mermaid.render)
Event, Execution, # runtime data
Driver, DurableRunner, # runners
ExecutionStore, DictStore, SqliteStore, # stores (durable extras: RedisStore, …)
ObservedStore, AsyncObservedStore, Step, # observing every commit
ContextError, EventError, ExpressionError, StoreConflict, PurgeRefused, # errors to catch
DefinitionChanged, DefinitionNotLoaded,
MachineResolver, DictResolver, FileResolver, ModuleResolver, SourceResolver, ResolveError,
)
Compiling¶
definition_from_dsl(text, name=None, *, base_path=None,
actions=None, guards=None, validate=False) -> Definition
definition_from_dsl_file(path, name=None, *, ...) -> Definition
nameselects the machine (required only if the file declares more than one).actions={"handler": fn_or_dotted_path}binds handlers; values may be callables or dotted paths. Wins over an in-DSLbind.guards={"name": {"field__op": value}}binds guards to predicate dicts. Wins over an in-DSLguard.validate=TrueraisesValidationErroron any error-severity issue at build time.base_pathis the directoryimports resolve against (defaults to the file’s directory for_from_dsl_file).The Definition carries the machine’s
version(1 when it declares none) and thedigestof its source.
machine_version(text, name, *, base_path=None) -> (version, digest) # what `harel hash` prints
definition_registry([defn, ...]) -> dict[str, Definition] # every version, for a runner
definition_registry keys each Definition by id@version, and by id for the highest version:
a new execution gets that one, an existing one the version it was created with. The same version
twice with different content raises ValueError. See
Changing a machine with executions in flight.
from harel.published import read_definitions, publish_definitions, apublish_definitions
publish_definitions(store, read_definitions("definitions/")) # what a worker does when it starts
# raises DefinitionConflict for a version published with other content
Validating¶
validate(defn) -> list[Issue] # Issue(code, severity, path, message)
validate_or_raise(defn) # raises ValidationError if any error-severity Issue
See static validation.
Running (single process)¶
DurableRunner(store, definitions, clock=time.time, resolver=None, trace=False,
*, execution="background", on_action_error="fail",
action_modules=(), actions=None, guards=None)
.create(definition_id, context=None, execution_id=None, priority=0, *, version=None) -> Execution
.process(execution_id, event) -> Execution
.fire_due_timers() -> int
.cancel(execution_id, *, reason=None) -> Execution
.terminate(execution_id) / .suspend(execution_id) / .resume(execution_id) -> Execution
.redrive(execution_id, target_path) -> Execution
.purge(execution_id, *, archive=None) -> bool # delete a finished tree; False if already gone
from harel.engine.control import purge_finished
purge_finished(store, *, older_than, statuses=(DONE, CANCELLED), archive=None,
include_undated=False, limit=None, dry_run=False, now=None) -> PurgeReport
# async stores: harel.engine.aio.control.purge_finished, same arguments, awaited
definitionsis a{definition_id: Definition}registry, ordefinition_registry(...)’s with several versions of a machine; inlineinvoketargets register automatically.create’sversionpicks a loaded version other than the highest. A version an execution needs anddefinitionslacks is read from the store’s published versions, binding its handlers and guards withactionsandguards(asdefinition_from_dsldoes);action_modulesare the modules such a version may call besides thosedefinitionsandactionscall.clockis injectable so durable timers fire deterministically.resolverresolves submachineinvokeFQNs not already indefinitions.trace=Truerecords the opt-in execution timeline in each commit (envHAREL_TRACE); off by default. See stores and the monitor.executionis"background"(a shared background event loop, the default) or"inline"(the caller’s own thread, no event loop, sync store only);on_action_erroris"fail"(dead-letter the execution) or"raise"(the exception reaches the caller, the step isn’t committed). See execution models.
Running (distributed)¶
from harel.engine.distributed import DistributedRunner
from harel.engine.transport import InMemoryTransport # + Sqlite/Libsql/Redis/Postgres/Rqlite/Mongo/Sqs
DistributedRunner(store, transport, definitions, clock=time.time, resolver=None, trace=False,
*, execution="background", action_modules=(), actions=None, guards=None)
.create(definition_id, context=None, execution_id=None, priority=0, start_on_create=True,
*, version=None) -> Execution
.start(execution_id, data=None) -> None # publishes Start; a worker runs it, not the caller.
# create()'s start_on_create=True already does this;
# data seeds the context — the only way to attach one to Start
.send(execution_id, event) -> None # refuses a caller-supplied Start (raises ValueError)
.worker(..., *, machines=None) -> Worker # .step() one message; .run(stop_event) loops;
# same `execution`; `machines`: claim only their work
.cancel / .terminate / .suspend / .resume / .redrive / .purge
createaccepts an optionalexecution_id(use an externally-supplied id, e.g. a Stripe PaymentIntent id, instead of a generated one; raisesExecutionAlreadyExistsif that id already exists — the create-or-find primitive) and apriority(0-4). Priority controls worker claim weighting underDistributedRunner(see distribution); on the single-processDurableRunnerit is stored on the Execution but has no effect (no transport to weight).
See distribution.
Key data types¶
Event(kind, data=None, id=None)— akind(name) plus an optional JSON-serializabledatapayload.Execution— the serializable run state:status,outcome,active_path,context,history,version. Round-trips to/from JSON.Definition— the compiled, immutable machine (node tree + index). Built from the DSL; rendered, validated, and executed by reference.
Resolvers (submachine invoke)¶
MachineResolver is the seam that maps an invoke FQN to a built Definition.
Inline invoke { … } targets and imported definitions resolve without one.
Four implementations ship out of the box:
Class |
Source |
|---|---|
|
in-memory registry (pre-built |
|
|
|
Python module attribute ( |
|
injected |
All four cache by FQN (each machine is compiled at most once). See the resolvers guide for usage examples and how to write a custom one.