Model or dataset
a-agmon/rs-graph-llm avatar
a-agmon/rs-graph-llm

graph-flow makes the next step a value you return, not a setting you configure

High-performance framework for building interactive workflow systems in Rust. Designed for complex workflows and multi-agent systems

378 stars42 forksRustMIT

At a glance

What is it?
A Rust framework for stateful, resumable agent workflows where each task returns a NextAction that decides whether control comes back to the caller or the engine keeps going, PostgreSQL arrives as a default feature, and the LLM half is delegated to Rig.
Who is it for?
Adopt graph-flow if you are building a stateful agent service in Rust and want per-step control, resumable sessions and typed state without writing the persistence loop yourself. Do not adopt it if you want a bundled agent runtime or a zero-dependency build, because the LLM integration is a separate crate and PostgreSQL is on by default.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 26 days ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 2, 2026, and from our analysis. They are not legal advice.

Editorial analysis

NextAction decides who runs the next task

The control model of graph-flow is one returned value. Every task returns a `NextAction`, and the variant decides what the engine does next.

`Continue` advances one edge and then hands control back to the caller. That is the interactive mode: after every hop the engine saves state and returns, which is what lets a web service answer the client between steps.

`ContinueAndExecute` is the opposite, described as fire-and-forget: the engine immediately runs the next task with the same context and keeps going until a task returns `Continue`, `WaitForInput` or `End`. Both fit in one line:

rust
// Step-by-step: caller decides when the next task runs
Ok(TaskResult::new(Some("Done".into()), NextAction::Continue))

// Continuous: single runner.run() call executes the whole chain
Ok(TaskResult::new(None, NextAction::ContinueAndExecute))

The other three are `WaitForInput`, which parks the workflow at the current task until input arrives, `GoTo(task_id)`, which jumps to a named task, and `End`.

What makes this more than a state machine is that the two modes can be blended freely. The documentation puts it as automated chains with interactive pauses in one workflow, which is the shape most real agent services end up wanting.

Tasks themselves are small: implement the `Task` trait, read and write shared state through `Context`, and return a result. The identifier defaults to the type name unless you override it.

A paused run carries the id of the task it will resume at

Workflows here are stateful in a specific sense. They can pause, wait for user input, and resume later, and they can do that across process restarts if the storage is persistent.

The quick start shows the shape. A session is created positioned at a task, context is set on it, and it is saved; then a loop calls the runner with the session id and prints the response.

rust
let storage = Arc::new(InMemorySessionStorage::new());
let runner = FlowRunner::new(graph.clone(), storage.clone());

let session = Session::new_from_task("session_001".to_string(), hello_task.id());
session.context.set("name", "Batman".to_string())?;
storage.save(session).await?;

What comes back is an execution result carrying the task's response and an `ExecutionStatus`. There are three states. `Paused { next_task_id, reason }` means the task finished and the workflow will proceed to that task on the next run, and it is what `Continue` and `GoTo` produce. `WaitingForInput` means the workflow is parked on the current task until you supply input and run again. `Completed` means it reached `End`. Failures come back as an `Err`.

Carrying the next task id inside the paused status is the detail that makes resumption work without a scheduler. The runner does not need to reconstruct where it was; the storage already says.

The convenience wrapper is three lines you could write yourself

`FlowRunner` is presented as a convenience, and the documentation says so unusually plainly: if you would rather drive things yourself, the lower-level API is exactly what the runner does internally.

That is three calls:

rust
let mut session = storage.get("session_001").await?.unwrap();
let result = graph.execute_session(&mut session).await?;
storage.save(session).await?;

Load the session, execute one step against it, save it back. The wrapper adds the loop and the id bookkeeping; nothing else.

Two situations justify dropping the wrapper, and both are named: custom persistence, and batching. If your sessions live somewhere the crate has no storage for, or you want to execute many sessions per transaction rather than one at a time, the three-line version is the interface you actually want.

It also makes the persistence contract visible. A session is a value you hold mutably while a step runs, and the state the next task sees is whatever the last task wrote into the context.

PostgreSQL is on by default, and that is worth arguing with

The feature flags are two, and the defaults are the interesting part.

`postgres` is enabled by default and provides `PostgresSessionStorage` along with its SQLx dependency. `rig` enables the Rig message integration independently of storage, so you can take the LLM bridge without the database, or the database without the LLM bridge.

Shipping a database driver as a default feature is a choice worth naming rather than accepting. The workspace pins SQLx at 0.8.6 with the tokio Rustls runtime, plus the postgres, json, macros and uuid features, and Tokio itself is pulled in with its `full` feature set. Any consumer that does not want a Postgres client in its dependency tree has to write `default-features = false`, and any consumer that wants it pays for it by default.

For a framework whose quick start uses `InMemorySessionStorage`, the default is arguably pointed the wrong way. The escape hatch is documented, which is what keeps it from being a real problem.

The dependency set otherwise looks like a service stack rather than a library: axum, tower and tower-http for the HTTP services, tracing with JSON output, dashmap, uuid, thiserror and anyhow.

The agent runtime is Rig's job, not this crate's

LLM integration here is a bridge, not an implementation, and the split is explicit.

The optional `rig` feature bridges `Context` chat history to Rig's `Message` type from `rig-core`. The agent runtime itself, the part with `Chat`, `Prompt` and `.agent()`, lives in the companion `rig-agent` crate. So the framework owns workflow execution, state and routing, and hands the model conversation to a crate that specialises in it.

The quick start shows the pairing, with both Rig crates pinned to the same version:

toml
[dependencies]
graph-flow = { version = "0.8", features = ["rig"] }  # drop "rig" if you don't need LLM helpers
rig-core = "0.42"                                     # provider clients + Message
rig-agent = "0.42"                                    # the agent runtime (Chat, Prompt, .agent())

Pinning rig-core and rig-agent to the same version is not incidental, since the bridge translates between their types.

The framing in the README is that graph-flow rebuilds natively in Rust the two ideas that make LangGraph pleasant: a graph execution engine for stateful workflows, and tight integration with an LLM ecosystem. The second one is delegated, and the feature flag is what keeps that delegation optional.

The service directories are the documentation that matters

The repository is a workspace with five members, and two of them are full services rather than demonstrations.

`insurance-claims-service/` is described as production-style: an HTTP service with LLM-driven claim intake, conditional routing and human-in-the-loop approval. That combination is the framework's thesis in one directory, since a claim that needs human approval is exactly the case where `WaitForInput` and `Paused` earn their place.

`recommendation-service/` is a RAG recommendation system with vector search, which is the retrieval half.

A third member, `medical-document-service/`, is in the workspace list in the root Cargo.toml but does not appear in the README's repository layout table, which lists only the framework, the two services and the examples. The workspace dependency `pdf2image` suggests why it exists, handling documents as images.

The `examples/` directory is a deliberate progression: `simple_example.rs` for the core concepts, then `complex_example.rs`, `recommendation_flow.rs`, `fanout_basic.rs` for parallel work, and `terminal_client.rs`. The README tells you to start with the simple one and read the services afterwards for real patterns, which is the correct order given how much the services assume.

Two snippets stop mid-statement, and the crate ships no releases

Two documentation gaps are visible in this copy, and neither is serious on its own.

The first code block, the `Task` implementation, ends after the comment about continuing to the next task but handing control back to the caller, with no closing braces and no return value. The `FlowRunner` loop block likewise stops after the `println!` that prints the response. Both need completing from the docs on docs.rs.

The second gap is the layout table mentioned above, where a workspace member is missing.

The project itself is in good shape. The licence is MIT, the repository is not archived, and the last push landed on 2026-09-07. There are no GitHub releases, which fits a crate published to crates.io instead, currently at version 0.8 in the quick start, with documentation published to docs.rs.

One dependency worth noting if you plan to build the services rather than the library: Tokio is pulled in with its `full` feature set at the workspace level, so a minimal consumer inherits far more of the async runtime than a typical library would ask for.

Editorial conclusion

Adopt graph-flow if you are building a stateful agent service in Rust and want per-step control, resumable sessions and typed state without writing the persistence loop yourself. Do not adopt it if you want a bundled agent runtime or a zero-dependency build, because the LLM integration is a separate crate and PostgreSQL is on by default. Read ExecutionStatus before designing your pause semantics, and start from examples/simple_example.rs rather than the service directories.

Frequently asked questions

What is GraphFlow?

In this repository, graph-flow is a stateful graph workflow framework for AI agents written in Rust, described as type-safe and LangGraph-inspired, for building complex, interactive and resumable agent workflows. The library is published to crates.io as graph-flow and documented on docs.rs.

How does graph-flow decide what runs next?

Each task returns a NextAction. Continue advances one edge and returns control to the caller, ContinueAndExecute keeps executing until something pauses, WaitForInput parks the workflow, GoTo jumps to a named task, and End completes it. The interactive and continuous modes can be mixed in one graph.

Can a graph-flow workflow pause and resume later?

Yes. Sessions are persisted through a SessionStorage, and an ExecutionStatus of Paused carries the next_task_id and a reason so the following run continues from there. With persistent storage such as PostgresSessionStorage, a workflow resumes across process restarts.

Do I need PostgreSQL to use graph-flow?

No, but it is on by default. The postgres feature provides PostgresSessionStorage and its SQLx dependency, and you exclude it with default-features = false. The quick start uses InMemorySessionStorage, and the rig feature works independently of storage.

Does graph-flow include an LLM agent runtime?

No. The optional rig feature bridges Context chat history to Rig's Message type from rig-core, while the agent runtime with Chat, Prompt and .agent() lives in the companion rig-agent crate. The quick start pins both Rig crates to 0.42.

Official sources

  1. a-agmon/rs-graph-llm on GitHub
  2. Issues
  3. License: MIT
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/a-agmon-rs-graph-llm.svg)](https://hysenlabs.com/projects/a-agmon-rs-graph-llm)