graphrun 0.1.1

Durable workflow engine library
Documentation
graphrun-0.1.1 has been yanked.

Graphrun

Graphrun is a Rust library. You depend on graphrun, open Engine::local on a directory in your process, and drive runs through that API. The library uses your Tokio runtime. It does not start a server, install signal handlers, or require a database daemon.

The graph is data. Write it with the typed builder or with YAML. Both compile to the same IR. Each command is committed through Raft, then applied to a redb file in that directory. Restart the process on the same path and the run is still there.

Install

[dependencies]
graphrun = "0.1.1"
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }

Rust 1.90 or newer. Linux and macOS.

Embed the engine

This is the whole local runtime: one Raft voter, in-process worker, Unix control socket under the data directory.

use graphrun::{Catalog, Engine, Value};
use std::collections::BTreeMap;
use std::time::Duration;

#[tokio::main]
async fn main() -> graphrun::Result<()> {
	let catalog = Catalog::from_json(include_bytes!("catalog.json"))?;
	let engine = Engine::local("./graphrun-data").await?;
	let input = Value::Object(BTreeMap::from([
		("order_id".into(), Value::String("o1".into())),
		("amount".into(), Value::Int(1000)),
	]));
	let run = engine
		.start_yaml(include_str!("sequence.yaml"), &catalog, input)
		.await?;
	let output = engine.wait_terminal(run, Duration::from_secs(10)).await?;
	engine.shutdown().await?;
	println!("{output:?}");
	Ok(())
}

start takes a compiled Definition from the builder. start_yaml compiles YAML against the same catalog. inspect, signal, cancel, and history are methods on Engine.

A complete listing, including catalog JSON, is graphrun/examples/order.rs. From this repo:

cargo run -p graphrun --example order

That prints payment_id: pay-1. The example activity names (inventory.reserve, payment.charge, and the other fixture names) have handlers inside the library. That is how the example finishes without a worker process of your own.

Typed builder

activity_ref checks input and output types against the catalog. A mismatch is a compile error.

use graphrun::builder::{RegionBuilder, WorkflowBuilder};
use graphrun::schema::{DurablePayload, SchemaRef};
use graphrun::Catalog;
use serde::{Deserialize, Serialize};

#[derive(Clone, Serialize, Deserialize)]
struct Order { order_id: String, amount: i64 }
impl DurablePayload for Order {
	fn schema_ref() -> SchemaRef { SchemaRef::named("order", 1).unwrap() }
}

#[derive(Clone, Serialize, Deserialize)]
struct ReservedOrder { order_id: String, amount: i64, reservation_id: String }
impl DurablePayload for ReservedOrder {
	fn schema_ref() -> SchemaRef { SchemaRef::named("reserved_order", 1).unwrap() }
}

#[derive(Clone, Serialize, Deserialize)]
struct Receipt { order_id: String, amount: i64, payment_id: String }
impl DurablePayload for Receipt {
	fn schema_ref() -> SchemaRef { SchemaRef::named("receipt", 1).unwrap() }
}

let catalog = Catalog::from_json(include_bytes!("catalog.json"))?;
let reserve = catalog.activity_ref::<Order, ReservedOrder>("inventory.reserve", 1)?;
let charge = catalog.activity_ref::<ReservedOrder, Receipt>("payment.charge", 1)?;
let mut root = RegionBuilder::<Order>::new();
let reserved = root.activity("reserve", &reserve, root.input())?;
let charged = root.activity("charge", &charge, reserved.output())?;
let root = root.complete("finish", charged.output())?;
let definition = WorkflowBuilder::new("sequence", 1, root).build(&catalog)?;
let run = engine.start(definition, catalog, input).await?;

The catalog is contracts: schema, activity key/version, execution kind, effect kind, recovery, error codes. It is not Rust type names. YAML for the same graph is docs/specs/v1/examples/sequence.yaml.

Cluster

Engine::member is a storage replica. Activity execution is opt-in. Engine::run_worker is a process that claims work over gRPC/mTLS and does not open a data directory. Local and clustered runs use the same write path. See Run a three-member cluster.

Optional CLI

graphrun-cli is an operator binary. It talks to an engine you already opened (--local-dir) or to a member (--endpoint plus mTLS). Your application does not depend on it.

cargo install graphrun-cli --locked
graphrun inspect --run <run-id> --local-dir ./graphrun-data

Limits

512 nodes per definition. YAML, payloads, and normalized IR are each 256 KiB. Unapplied log: 4,096 entries or 64 MiB. Full table: docs/limits.md.

How-tos

Spec: docs/specs/v1.

Development

cargo fmt --all -- --check
cargo clippy --workspace --all-targets --locked -- -D warnings
cargo test --workspace --locked -- --skip snapshot_controller_fires
cargo build --release --locked -p graphrun-cli
cargo run --locked -p graphrun-e2e -- verify \
	--cli target/release/graphrun \
	--matrix docs/specs/v1/verification-matrix.tsv \
	--artifacts target/e2e-artifacts