Graphrun
Graphrun is a durable workflow engine. You write a graph in YAML or with a typed Rust builder. Both compile to the same IR. Every command goes through Raft, then a redb apply. Kill the process. Open the same data directory again. The run is still there.
Local mode is one Raft voter in-process. A cluster uses the same write path with more voters and optional remote workers. There is no in-memory shortcut.
Install
Library:
[]
= "0.1"
= { = "1", = ["rt-multi-thread", "macros"] }
CLI (installs the graphrun binary):
Rust 1.90 or newer. Linux and macOS.
Run a workflow
Engine::local owns a data directory. This graph reserves inventory, then charges payment. Built-in handlers in the library implement those two activity names.
dsl: graphrun/v1
id: sequence
version: 1
input_schema: order/v1
output_schema: receipt/v1
start: reserve
nodes:
reserve:
kind: activity
activity:
input:
retry:
errors:
max_attempts: 5
next: charge
charge:
kind: activity
activity:
input:
next: finish
finish:
kind: complete
output:
use ;
use BTreeMap;
use Duration;
async
The output is payment_id: pay-1.
A complete, runnable copy (catalog JSON included) is graphrun/examples/order.rs:
The catalog is contracts: schema, activity name/version, execution kind, effect kind, recovery, and error codes. YAML and the builder both require it. The example catalog and more graphs live in docs/specs/v1/examples.
Build the same graph in Rust
activity_ref binds input and output types. A mismatch fails at compile time.
use ;
use ;
use Catalog;
use ;
let catalog = from_json?;
let reserve = catalog.?;
let charge = catalog.?;
let mut root = new;
let reserved = root.activity?;
let charged = root.activity?;
let root = root.complete?;
let definition = new.build?;
Then engine.start(definition, catalog, input).await. YAML and builder produce one IR. cargo test --test api yaml_and_builder_while_match checks that parity.
Add serde = { version = "1", features = ["derive"] } for the payload types.
CLI
&
start waits for a terminal status unless you pass --no-wait. Inspect prints status, output, blocked_reason, pending waits, and open scopes.
To deliver an external event:
--key is the wait correlation key. Certificate private keys use --tls-key.
Other commands: cancel, list, history, replay, resolve (blocked compensation, abandon), backup, restore, cluster join|promote|remove.
A cluster member is reached with --endpoint, --ca, --cert, --tls-key, and --server-name instead of --local-dir.
What the engine does
- One write path. Command, Raft quorum, decide, events, evolve, redb apply. Local and clustered runs share that path.
- Activities. Catalog contracts are data.
Engine::localruns the library handlers for the names in the example catalog (inventory.reserve,payment.charge,counter.increment, and the other fixture names). Those handlers are how the examples complete without a separate worker process. - Waits and signals. A run can block on a named signal and a correlation key.
EventIdis a 32-character hex identity. Duplicate ids are idempotent. - Sagas. A failed run compensates open obligations in reverse order. If compensation input cannot be built, the obligation is
blockeduntilresolvesupplies input or--abandon --confirm. - History.
inspectandhistoryare read-only. Replay reconstructs state from recorded events. Apply does not call activities, clocks, or RNG.
Cluster
Three members plus workers is the production shape. Members speak Tonic gRPC over mTLS. Workers claim ready activities from a member. See Run a three-member cluster.
Limits
A definition may have 512 nodes. YAML, payloads, and the normalized IR are each capped at 256 KiB. Unapplied log is capped at 4,096 entries or 64 MiB. The full table is docs/limits.md.
How-tos
- Run a local YAML workflow
- Build a workflow in Rust
- Run a three-member cluster
- Deliver an external event
- Compensate, intervene, or abandon
- Back up and restore a member
- Diagnose a stalled run
The v1 spec is docs/specs/v1.
Development