graphrun 0.1.2

Durable workflow engine library
Documentation

Graphrun

Graphrun is an embedded workflow engine for Rust. Depend on graphrun, open Engine::local on a directory in your process, and drive runs through that API. It uses your Tokio runtime. It does not start a server or need a database daemon.

The graph is data: YAML or a typed builder, both the same IR. Commands go through Raft onto a redb file in that directory. Restart the process on the same path and the run is still there.

This release does not let you plug in your own activity handlers. A catalog names contracts (counter.increment, inventory.reserve, …). Local mode runs built-in fixtures for those names. An unknown name echoes its input and succeeds. Use Graphrun to persist and orchestrate those graphs; do not treat a custom catalog name as user code that ran.

Install

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

Rust 1.90 or newer. Linux and macOS. 0.1 resolves to the latest unyanked 0.1.x on crates.io.

First run

Paste this into src/main.rs. Input {value: 0} finishes at {value: 2}.

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

const CATALOG: &[u8] = br#"{
  "format": "graphrun.catalog/v1",
  "schemas": {
    "counter/v1": {
      "type": "object",
      "required": ["value"],
      "additionalProperties": false,
      "properties": {"value": {"type": "integer"}}
    }
  },
  "activities": [
    {
      "name": "counter.increment",
      "version": 1,
      "input_schema": "counter/v1",
      "output_schema": "counter/v1",
      "execution": "async",
      "effects": "pure",
      "recovery": "RetrySafe",
      "error_codes": []
    }
  ]
}"#;

const YAML: &str = r#"
dsl: graphrun/v1
id: hello_world
version: 1
input_schema: counter/v1
output_schema: counter/v1
start: hello
nodes:
  hello:
    kind: activity
    activity: {name: counter.increment, version: 1}
    input: {from: workflow.input}
    next: goodbye
  goodbye:
    kind: activity
    activity: {name: counter.increment, version: 1}
    input: {from: nodes.hello.output}
    next: finish
  finish:
    kind: complete
    output: {from: nodes.goodbye.output}
"#;

#[tokio::main]
async fn main() -> graphrun::Result<()> {
	let catalog = Catalog::from_json(CATALOG)?;
	let engine = Engine::local("./graphrun-data").await?;
	let input = Value::Object(BTreeMap::from([("value".into(), Value::Int(0))]));
	let run = engine.start_yaml(YAML, &catalog, input).await?;
	let output = engine.wait_terminal(run, Duration::from_secs(10)).await?;
	println!("{output:?}");
	engine.shutdown().await?;
	Ok(())
}

A catalog is the registry of payload schemas and activity contracts the graph may name. It is not the workflow and not your handler code.

From this repository, the same idea as a crate example:

cargo run -p graphrun --example order

That prints payment_id: pay-1 (inventory.reserve then payment.charge).

What to read next

Do these in order. Each sample folder is self-contained (workflow.yaml, catalog.json, payload types, builder).

  1. 01 Hello World — the graph above, plus the typed builder
  2. 02 Passing Data — order → reserve → charge
  3. 03 Events — wait_signal and Engine::signal

Then pick from 04–10: while, foreach, choose, parallel, saga, repeat, timeout recovery.

cargo run -p graphrun-samples --bin 01-hello-world

How-tos:

Typed builder

payload!(Order, "order") binds a struct to catalog schema order/v1. First .activity uses the run input; the next uses the previous output. Same IR as the YAML.

The full listing is samples/02-passing-data.

let reserve = catalog.activity_v1::<Order, ReservedOrder>("inventory.reserve")?;
let charge = catalog.activity_v1::<ReservedOrder, Receipt>("payment.charge")?;
let definition = workflow::<Order>("passing_data")
	.activity("reserve", &reserve)?
	.activity("charge", &charge)?
	.finish(&catalog)?;
let run = engine.start(definition, catalog, input).await?;

Optional CLI

graphrun-cli inspects and signals an engine you already opened. Your application does not depend on it.

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

Cluster

After a local run works: Engine::member is a storage replica, Engine::run_worker claims activities over gRPC/mTLS. Same write path as local. Cluster how-to.

Limits

A definition may have 512 nodes. YAML, payloads, and IR are each 256 KiB. docs/limits.md.