rs-graph-llm
Library Listed
A high-performance, type-safe framework for building multi-agent workflow systems in Rust
- Downloads
- 5,308
- Dependents
- 8
- Stars
- 388
- Forks
- 43
- Contributors
- 6
- Commits
- 87
- Latest release
- 0.8.1 ·
About

graph-flow
Stateful Graph Workflow Framework for AI Agents in Rust
A type-safe, LangGraph-inspired framework for building complex, interactive, resumable agent workflows
graph-flow combines the two ideas that make LangGraph pleasant to use — a graph execution engine for stateful workflows, and tight LLM ecosystem integration — and rebuilds them natively in Rust:
graph-flow— the core graph execution library: task orchestration, session persistence, conditional routing, human-in-the-loop pauses- Rig — Rust-native LLM integration. The optional
rigfeature bridgesContextchat history torig-core'sMessage; the agent runtime itself lives in the companionrig-agentcrate
You get LangGraph-style workflow design with Rust's performance and type safety, a clean database schema for session state, and flexible execution models — step-by-step, fire-and-forget, or a mix of both in the same graph.
Read the full README
Repository Layout
| Crate | What it shows |
|---|---|
graph-flow/ | The framework library (published to crates.io) |
insurance-claims-service/ | Production-style HTTP service: LLM-driven claim intake, conditional routing, human-in-the-loop approval |
recommendation-service/ | RAG recommendation system with vector search |
examples/ | Small progressive examples: simple_example, complex_example, recommendation_flow, fanout_basic, terminal_client |
Start here: read
examples/simple_example.rsfor the core concepts, then the services for real-world patterns.
Cargo features
postgres(enabled by default): providesPostgresSessionStorageand its SQLx dependency.rig: enables Rig message integration independently of storage.
PostgreSQL support is enabled by default; set default-features = false to exclude it.
Quick Start
[dependencies]
graph-flow = { version = "0.8", features = ["rig"] } # drop "rig" if you don't need LLM helpers
tokio = { version = "1", features = ["macros", "rt-multi-thread"] } # your async runtime; graph-flow enables only what it uses
rig-core = "0.42" # provider clients + Message
rig-agent = "0.42" # the agent runtime (Chat, Prompt, .agent())
1. Define tasks
Tasks are the building blocks of a workflow. Each implements the Task trait, reads and writes shared state through Context, and returns a NextAction that steers the graph:
use async_trait::async_trait;
use graph_flow::{Context, NextAction, Task, TaskResult};
struct HelloTask;
#[async_trait]
impl Task for HelloTask {
// id() defaults to the type name; override it for a custom identifier
async fn run(&self, context: Context) -> graph_flow::Result<TaskResult> {
let name: String = context.get("name").unwrap_or_default();
let greeting = format!("Hello, {name}");
// Store result for the next task
context.set("greeting", greeting.clone())?;
// Continue to the next task, but hand control back to the caller
Ok(TaskResult::new(Some(greeting), NextAction::Continue))
}
}
2. Build the graph
use graph_flow::GraphBuilder;
use std::sync::Arc;
let hello_task = Arc::new(HelloTask);
let excitement_task = Arc::new(ExcitementTask);
let graph = Arc::new(
GraphBuilder::new("greeting_workflow")
.add_task(hello_task.clone())
.add_task(excitement_task.clone())
.add_edge(hello_task.id(), excitement_task.id())
.build()?, // validates edges and start task
);
3. Execute with sessions
Workflows are stateful: they can pause, wait for user input, and resume later — across process restarts if you use persistent storage. FlowRunner wraps the load → execute one step → save cycle:
use graph_flow::{ExecutionStatus, FlowRunner, InMemorySessionStorage, Session, SessionStorage};
use std::sync::Arc;
let storage = Arc::new(InMemorySessionStorage::new());
let runner = FlowRunner::new(graph.clone(), storage.clone());
// Create a session positioned at the first task
let session = Session::new_from_task("session_001".to_string(), hello_task.id());
session.context.set("name", "Batman".to_string())?;
storage.save(session).await?;
// Drive the workflow step by step
loop {
let result = runner.run("session_001").await?;
println!("Response: {:?}", result.response);
match result.status {
ExecutionStatus::Completed => break,
ExecutionStatus::Paused { .. } => continue, // will auto-continue to the next task
ExecutionStatus::WaitingForInput => break, // collect user input, set it in context, run again
}
}
Prefer manual control (custom persistence, batching)? Use the lower-level API — it is exactly what FlowRunner does internally:
let mut session = storage.get("session_001").await?.unwrap();
let result = graph.execute_session(&mut session).await?;
storage.save(session).await?;
Execution Control
You choose how the graph advances by returning the right NextAction from each task:
NextAction | Behavior |
|---|---|
Continue | Advance one edge, then return control to the caller (step-by-step) |
ContinueAndExecute | Advance and keep executing tasks in the same call until something pauses |
WaitForInput | Pause at the current task until new input arrives |
GoTo(task_id) | Jump to a specific task |
End | Complete the workflow |
Continue is the interactive mode: after every hop the engine saves state and returns, so a web service can respond to the client between steps.
ContinueAndExecute is 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. Blend both freely — automated chains with interactive pauses in one workflow.
// 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))
ExecutionStatus
Every execution returns an ExecutionResult with the task's response and a status:
Paused { next_task_id, reason }— task finished; the workflow will proceed tonext_task_idon the next run (returned forContinueandGoTo)WaitingForInput— workflow is parked on the current task until you provide input and run againCompleted— the workflow reachedEnd
Failures are reported as Err(GraphError) from the execution call itself.
Guard rails
Two builder options protect long-running graphs:
let graph = GraphBuilder::new("wf")
.add_task(task)
.with_task_timeout(std::time::Duration::from_secs(60)) // per-task timeout (default: 5 min)
.with_max_execution_steps(50) // cap chained ContinueAndExecute steps (default: unlimited)
.build()?;
with_max_execution_steps turns an accidental ContinueAndExecute cycle into an error instead of an infinite loop.
Advanced Features
Conditional edges
Branch at runtime on data in the Context (complex_example.rs):
let graph = GraphBuilder::new("sentiment_flow")
.add_task(sentiment_task.clone())
.add_task(positive_task.clone())
.add_task(negative_task.clone())
.add_conditional_edge(
sentiment_task.id(),
|ctx| ctx.get::<String>("sentiment").map(|s| s == "positive").unwrap_or(false),
positive_task.id(), // yes branch
negative_task.id(), // else branch
)
.build()?;
graph TD
SA["SentimentAnalysisTask"] --> S{Sentiment?}
S -->|"positive"| P["PositiveResponseTask"]
S -->|"negative"| N["NegativeResponseTask"]
LLM integration with Rig
With the rig feature, chat history stored in the Context converts directly to Rig messages:
// `graph-flow`'s `rig` feature needs only `rig-core` (for `Message`).
// The agent runtime — `Chat`, `Prompt`, `.agent()` — lives in `rig-agent`.
use rig_agent::prelude::*;
use rig_core::providers::openrouter;
async fn run(&self, context: Context) -> graph_flow::Result<TaskResult> {
let user_input: String = context.get("user_input").unwrap();
let client = openrouter::Client::new(&api_key)
.map_err(|e| graph_flow::GraphError::TaskExecutionFailed(e.to_string()))?;
let agent = client
.agent("openai/gpt-4o-mini")
.preamble("You are a helpful insurance assistant")
.build();
// Conversation context for the LLM. `chat` seeds the request from this
// vector and appends the turn's committed messages back into it.
let mut chat_history = context.get_rig_messages();
let response = agent.chat(&user_input, &mut chat_history).await?;
// Persist the exchange
context.add_user_message(user_input);
context.add_assistant_message(response.clone());
Ok(TaskResult::new(Some(response), NextAction::Continue))
}
Context and state management
Context is thread-safe, typed, and fully serialized with the session:
All Context methods are synchronous (the store is an in-memory map), so the same calls work in async tasks and in edge-condition closures:
// Typed key-value state; set() is fallible (serialization can fail)
context.set("claim_amount", 1500.0)?;
let amount: f64 = context.get("claim_amount").unwrap();
// Works directly in edge conditions
let ok = context.get::<bool>("condition").unwrap_or(false);
// Built-in chat history (ring buffer, capped at 1000 messages by default)
context.add_user_message("What's my claim status?".to_string());
context.add_assistant_message("Your claim is being processed".to_string());
let recent = context.get_last_messages(5);
Parallel execution (FanOut)
FanOutTask runs child tasks concurrently within one graph node, waits for all of them, and aggregates their outputs into the context under prefixed keys (fanout_basic.rs):
use graph_flow::FanOutTask;
let fanout = FanOutTask::new("fanout", vec![Arc::new(ChildA), Arc::new(ChildB)])
.with_prefix("parallel");
// FanOut is a regular task in the graph
let graph = GraphBuilder::new("fanout_demo")
.add_task(prepare_task.clone())
.add_task(fanout.clone())
.add_task(consume_task.clone())
.add_edge(prepare_task.id(), fanout.id())
.add_edge(fanout.id(), consume_task.id())
.build()?;
// Downstream tasks read: parallel.child_a.response, parallel.child_b.response, ...
Caveats: children's NextAction is ignored (the FanOut node controls flow), all children share one context, and if any child fails the FanOut fails with the first error (other children still complete, so partial results may be present).
Storage backends
Both backends implement the same SessionStorage trait — swap freely between development and production:
// In-memory (development)
let storage = Arc::new(InMemorySessionStorage::new());
// PostgreSQL (requires the `postgres` feature)
let storage = Arc::new(PostgresSessionStorage::connect(&database_url).await?);
// Or bring your own pool configuration
let pool = sqlx::postgres::PgPoolOptions::new().max_connections(20).connect(&url).await?;
let storage = Arc::new(PostgresSessionStorage::with_pool(pool).await?);
Case Study: Insurance Claims Processing
The insurance-claims-service is a complete agentic workflow behind an Axum HTTP API:
graph TD
A["Initial Claim Query<br/>LLM conversation"] --> B["Insurance Type Classifier"]
B --> C{Insurance Type?}
C -->|"Car"| D["Car Insurance Details"]
C -->|"Apartment"| E["Apartment Insurance Details"]
D --> F["Smart Claim Validator"]
E --> F
F --> G{Claim Amount?}
G -->|"< $1000"| H["Auto-Approve"]
G -->|"≥ $1000"| I["Manual Review<br/>(waits for human input)"]
I --> K{Approval Decision?}
K -->|"approved"| L["Manual Approval"]
K -->|"other"| M["Final Summary"]
H --> M
L --> M
It demonstrates the framework's key patterns working together:
- LLM-first tasks — every interactive step uses an agent for natural-language understanding (
initial_claim_query.rs) - Structured extraction — tasks parse JSON out of LLM responses into typed context values
- Content-based routing — conditional edges route on the LLM's classification (
insurance_type_classifier.rs) - Human-in-the-loop — claims ≥ $1000 return
WaitForInputand park the session until a reviewer responds (smart_claim_validator.rs) - Full state tracking — chat history, structured claim data, workflow position, and status messages survive across requests
Running it
export OPENROUTER_API_KEY="your-key"
export DATABASE_URL="postgresql://user:pass@localhost/db" # optional, defaults to in-memory
cargo run --bin insurance-claims-service
# Start a claim
curl -X POST http://localhost:3000/execute \
-H "Content-Type: application/json" \
-d '{"content": "I need to file a claim for my car accident"}'
# Continue the conversation (use the session_id from the response)
curl -X POST http://localhost:3000/execute \
-H "Content-Type: application/json" \
-d '{"session_id": "uuid-here", "content": "It happened yesterday on Main Street"}'
# Inspect session state
curl http://localhost:3000/session/uuid-here
The HTTP handler is one workflow step per request:
async fn execute_graph(
State(state): State<AppState>,
Json(request): Json<ExecuteRequest>,
) -> Result<Json<ExecuteResponse>, StatusCode> {
// Store the user's message in the session context
let session = state.session_storage.get(&session_id).await?;
session
.context
.set("user_input", request.content)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
state.session_storage.save(session).await?;
// Execute one step; FlowRunner loads, runs, and saves the session
let result = state.flow_runner.run(&session_id).await?;
Ok(Json(ExecuteResponse {
session_id,
response: result.response,
status: format!("{:?}", result.status),
}))
}
Production Considerations
- Per-session concurrency: sessions carry an optimistic-locking version. If two runs of the same session race, the losing save fails with
GraphError::SessionConflictinstead of silently overwriting — reload and retry on conflict. Task side effects from the losing attempt are not rolled back, so keep tasks idempotent if concurrent runs are possible. - Timeouts and step limits: set
with_task_timeoutto bound slow LLM calls, andwith_max_execution_stepsif your graph contains cycles reachable viaContinueAndExecute. - Durability of chained steps: within one
ContinueAndExecutechain, the session is saved only after the chain finishes — a crash mid-chain re-runs from the chain's first task.
API Stability
0.6.0 completed the API cleanup planned during the 0.5 engine review: synchronous fallible Context methods, validated immutable graphs (build() returns Result), optimistic session locking, and removal of the deprecated Graph::execute, NextAction::GoBack, and ExecutionStatus::Error. The full list and a 0.5 → 0.6 migration guide live in graph-flow/ROADMAP.md.
License
MIT License — see LICENSE.
Compatibility
- Runs on
- GPU support unknown
What shows it
- no dependency or feature matches a backend row
- Native code
- None found
- No native code found
- Python bindings
- No Python binding crate found
- License
- MIT
Compare with similar projects
Order: strongest tier first, then most downloads, then name, with unmeasured values last. A library counts downloads over 30 days. An app counts binary downloads per release. An app with none counts its crate downloads over 30 days. ML Rust runs this catalogue, so its closest project always gets a row.
| Project | Downloads | Dependents | Stars | Latest release | Runs on (GPU) | Native code | License |
|---|---|---|---|---|---|---|---|
|
A high-performance, type-safe framework for building multi-agent workflow systems in Rust |
5,308 downloads over 30 days | 8 | 388 | 0.8.1 | GPU support unknown | No native code found | MIT |
|
rust-mcp-schema Featured
Type-safe implementation of the Model Context Protocol in Rust, designed to reduce errors and accelerate development with powerful utilities. |
107,187 downloads over 30 days | 19 | 79 | 2.0.0 | GPU support unknown | No native code found | MIT |
|
rust-mcp-sdk Featured
An asynchronous SDK and framework for building MCP-Servers and MCP-Clients, leveraging the rust-mcp-schema for type safe MCP Schema Objects. |
29,764 downloads over 30 days | 51 | 199 | 2.0.0 | GPU support unknown | No native code found | MIT |
|
autoagents Listed
Agent Framework for Building Autonomous Agents |
8,627 downloads over 30 days | 4 | 779 | 0.4.0 | CUDA, WebGPU, Metal, ROCm, Vulkan | Binds native code: llama-cpp-sys-2, ort-sys, alsa-sys (feature audio-capture), intel-mkl-src (feature full), rquickjs-sys (feature codeact) | Apache-2.0 OR MIT |
|
swarms-rs Listed
The Bleeding-Edge Production-Ready Multi-Agent Orchestration Framework in Rust |
130 downloads over 30 days | 1 | 193 | 0.3.0 | GPU support unknown | Binds native code: openssl-sys, zstd-sys | Apache-2.0 |
|
autogpt Listed
🦀 A Pure Rust Framework For Building AGIs. |
2 downloads over 30 days | 0 | 117 | 0.4.5 | GPU support unknown | Binds native code: ring, zstd-sys, libgit2-sys (feature git), libz-sys (feature git) | MIT |
|
An open protocol that gives AI agents a secure, sandboxed place to run code and use tools — with lean context usage. AXP works alongside MCP |
Not measured | Not applicable | 3 | Not measured | Not measured | Not measured | Apache-2.0 |
Why ListedNo archive, New or Featured rule places it, so it is Listed.
What it lacks for Featured
- It has 1 active dependents and 1 new dependents in 365 days. Featured needs 11 active dependents or 9 new dependents in 365 days. The repository route reads 388 stars, 43 forks and 6 contributors. It needs 4,539 stars, 434 forks and 79 contributors together.
What else the check found
- The last activity is on 21 Oct 2026, 19 days ago. The rules expect an update within 365 days. Dormancy rule
Exact rules: Listed, Featured library. The check on applied thresholds version 9.
Every rule outcome, as the check records it
expected-interval: version 0.8.1 base 300, raised to the 365-day floor: E = 365 daysdormancy: last activity 2026-10-21, 19 days ago, E = 365: activefeatured: not featured: active_users 1 < 11 and new_adopters 1 < 9 and repository route stars 388 (needs 4539), forks 43 (needs 434), contributors 6 (needs 79)listed: listed: no archive, new or featured rule applies
Measurements and sources
Each signal row shows the raw value, its source and the fetch date. The Sources list gathers every distinct source on this page. Movement needs more history.
Sources6 sources
- Description: the crates.io description
- crates.io database dump, fetched
- RustSec advisory database, fetched
- GitHub repository, fetched
- crates.io package, fetched
- GitHub contributors, fetched
Adoption1 active dependents, 1 new dependents in 365 days1 not measured
| Signal | Value | Source | Fetched | Counts |
|---|---|---|---|---|
| Active dependents | 1 | crates.io database dump | Yes | |
| New dependents (365 days) | 1 | crates.io database dump | Yes | |
| Peak active dependents | 1 | crates.io database dump | Yes | |
| Active dependents as a share of the peak | 1 | crates.io database dump | Yes | |
| Binary downloads per release | Not measured | Yes |
MaintenanceLatest human commit: 19 days ago3 not measured
| Signal | Value | Source | Fetched | Counts |
|---|---|---|---|---|
| Latest human commit | 2026-10-21 | GitHub repository | Yes | |
| Latest release | 2026-10-21 | crates.io database dump | Yes | |
| First release | 2025-06-29 | crates.io database dump | Yes | |
| Latest version | 0.8.1 | crates.io database dump | Yes | |
| Days the oldest outside issue waits for a reply | 0 | GitHub repository | Yes | |
| Maintainer replies over 365 days | 11 | GitHub repository | Yes | |
| Repository archived | false | GitHub repository | Yes | |
| Repository gone | false | GitHub repository | Yes | |
| Release commit or manifest in the repo | true | crates.io package | Yes | |
| Published docs | true | crates.io database dump | Yes | |
| Latest release fails cargo check | Not measured | Yes | ||
| Build check date | Not measured | Yes | ||
| Build check toolchain | Not measured | Yes |
SecurityUnpatched vulnerabilities: none
| Signal | Value | Source | Fetched | Counts |
|---|---|---|---|---|
| Unpatched vulnerabilities past the grace window | none | RustSec advisory database | Yes | |
| RustSec unmaintained advisory | none | RustSec advisory database | Yes | |
| Deprecation notice | none | crates.io database dump | Yes | |
| Deprecated requirement | none | crates.io database dump | Yes |
Distribution15 releases, 5,308 downloads over 30 days5 not measured
| Signal | Value | Source | Fetched | Counts |
|---|---|---|---|---|
| Releases | 15 | crates.io database dump | Yes | |
| Downloads over 30 days | 5308 | crates.io database dump | Yes | |
| Binary downloads over the last 3 releases | Not measured | Yes | ||
| Homebrew installs on request over 365 days | Not measured | Yes | ||
| Repology repository families | Not measured | Yes | ||
| Docker pull growth over 90 days | Not measured | Yes | ||
| Docker pulls at the growth baseline | Not measured | Yes | ||
| Every version a prerelease | false | crates.io database dump | Yes | |
| Every version 0.0.x | false | crates.io database dump | Yes | |
| Every version yanked | false | crates.io database dump | Yes | |
| Crate deleted from crates.io | false | crates.io database dump | Yes |
Repository388 stars, 43 forks, 6 contributors, 87 commits. Stars alone never qualify a project.
| Signal | Value | Source | Fetched | Counts |
|---|---|---|---|---|
| GitHub stars | 388 | GitHub repository | Yes — one part of the repository route | |
| GitHub forks | 43 | GitHub repository | Yes — one part of the repository route | |
| Contributors with a GitHub account | 6 | GitHub contributors | Yes — one part of the repository route | |
| Commits on the default branch | 87 | GitHub repository | No | |
| Repository's main crate left out of the entry | none | crates.io database dump | Yes |
Facts that never count
| Signal | Value | Source | Fetched | Counts |
|---|---|---|---|---|
| License | MIT | crates.io database dump | No | |
| Runs on, from listed crates and their direct dependencies | GPU support unknown | crates.io database dump | No | |
| What shows each backend | no dependency or feature matches a backend row | crates.io database dump | No | |
| Native code, from listed crates and their dependencies | No native code found | crates.io database dump | No | |
| Python bindings, from direct dependencies of listed crates | No Python binding crate found | crates.io database dump | No | |
| Crates that depend on a listed crate directly | 8 | crates.io database dump | No |
Corrections and review history
No correction applies to this entry.
Judgments
A judgment enters only through an approved issue and a merged pull request.
No judgment applies to this entry.