Loams Graph
A property graph database with GQL, embedded in the Loams engine on Loams' fork of Grafeo, with its writes in the Loams log and its snapshots in the bucket.
Loams Graph is a property graph database inside the Loams engine. You create a graph in a namespace and query it with GQL, the ISO/IEC 39075 graph query language, over the loams.graph.v1 Connect API on the engine's main port. The engine is Grafeo, an embedded Rust graph engine, built from Loams' own fork. Grafeo runs the queries. It is not the graph's durable copy. Every committed write becomes a record in the graph's stream in the Loams log before the client gets an answer, and snapshots of the graph go to your bucket. If a node dies, another node rebuilds the graph from the newest snapshot plus the log after it.
Use it for graph-shaped data that lives next to the rest of your Loams data: entity and relation graphs for GraphRAG, dependency and ownership graphs, and graphs a link keeps up to date from a collection or a stream. Do not use it if your code or tools speak Cypher over Bolt. Loams Graph has no Bolt endpoint and no Cypher, and Neo4j drivers, the Neo4j browser and APOC procedures will not work against it. Do not put a graph on it that needs more than one machine's memory either: each graph has one owner node, and sharded graphs are out of scope for this design. If you need a mature graph database with a large tool ecosystem today, Neo4j is the safer choice (see the comparison below).
Status
Loams Graph is Available in Loams 0.1.0, the current release. It is compiled into the release loams image (cargo feature graph) and served on the main port. The 0.1.0 feature table describes it like this: "Writes reach the Loams log before they are acknowledged, with a fenced single owner per graph, snapshots in the bucket and crash recovery (GR1 Tasks 11 to 14), on Loams' Grafeo fork". Its known limitation is just as plain: keep a copy of graph data you cannot recreate. The wire package loams.graph.v1 is marked unstable, which means it can change until the production exit (GR1 Task 39).
| Piece | Since | |
|---|---|---|
loams.graph.v1: graph admin and GQL Execute, ExecuteBatch, ExecuteStream, Explain (GR1a) | preview.1 | Available |
| Writes in the Loams log before acknowledgement (GR1 Task 11) | preview.1 | Available |
| One fenced owner per graph, forwarding (Task 12); snapshots and the graph manifest (Task 13); recovery, rehydration, idle eviction (Task 14) | preview.2 | Available |
Loams' Grafeo fork, ostrium-labs/grafeo v0.5.43-loams.1 | preview.2 | Available |
Linked graphs, Search.expand for GraphRAG, SQL graph_* functions, graph algorithms (GR1 Tasks 17 to 22) | merged on dev; the 0.1.0 table still lists graph expansion as planned | Experimental |
| Quotas, metrics, standbys and failover, point-in-time restore, the Cypher dialect, SDK facades (GR1 Tasks 25 to 39) | GR1 plan | Planned |
| Agent memory on graphs, Loams Memory (design 52, plan MM1) | design only | Planned |
The 0.1.0 changelog has no graph entries. The durability work arrived in preview.2: "A fenced single owner per graph with forwarding between graph nodes (GR1 Task 12, D744), snapshots in the bucket with a graph manifest (Task 13), and crash recovery, rehydration and idle eviction (Task 14)."
Architecture
Each graph lives on one node at a time, the owner. Placement uses rendezvous hashing over the live nodes that have the graph role. The owner holds the graph open in an embedded Grafeo instance and serves strong reads and all writes. Its truth lives in two places outside that process: the graph's stream _graph.<id> in the Loams log, and snapshots under ns/<ns_id>/graphs/<gr_id>/ in the bucket. Both survive the node. This is the log is the spine and bucket as source of truth pattern, applied to a graph engine that was built to keep its own files.
How it works
GQL on Grafeo
Statements are GQL. The build compiles in Grafeo's GQL parser and nothing else. Grafeo can also speak Cypher, Gremlin, GraphQL, SPARQL and SQL/PGQ, but those are compiled out, and a request in another language gets graph_language_disabled. The declared profile is gql-1 (D747). Before a statement runs, a keyword guard and Grafeo's own classifier both decide whether it reads or writes. A conformance corpus checks that the guard never calls something a read when Grafeo calls it a write. DDL is refused for now.
The API is two Connect services in proto/loams/graph/v1/graph.proto:
GraphAdminService:CreateGraph,GetGraph,ListGraphs,UpdateGraph,DeleteGraph,GetSchema,GetEngineInfo, plusRestoreGraph,ExportGraphandImportGraph, which are declared but are open work in GR1 Task 29.GraphService:Execute,ExecuteBatch,ExecuteStream(server streaming) andExplain.
Two server flags bound the work: --graph-query-timeout-ms (default 30000, at most 300000) and --graph-statement-slots (twice the cores, at most 32).
The durable write path
Grafeo's own write-ahead log defaults to periodic fsync and never reaches the bucket, so Loams does not trust Grafeo's files (D742). Each graph has a write lane:
- A write transaction runs in Grafeo under a per-graph gate.
- At commit, its changes are captured as one
GraphChangeSet, a hand-encoded, engine-neutral record (format inconformance/graph/records/v1). - The lane group-commits records into the graph's stream. A group closes when the queue is empty, after 2 ms, at 256 KiB, or at 1,024 records. One record holds at most 32 MiB.
- Only after the append is durable does the client get its answer.
The stream _graph.<id> has one partition. Its name is reserved, so no user can create it or write into it. The log's batches carry a CRC32C. Records do not carry their own.
One fenced owner (D744)
A graph has exactly one writer. The owner holds a metastore lease, graph/<gr_id>/owner, with a 10 s TTL, renewed every 3 s. The lease carries an epoch. Every append to the graph's stream is a fenced commit: the metastore checks the epoch atomically when it assigns the offset. An append under a stale epoch fails with Fenced. The node that gets that refusal marks the graph fenced, stops serving it, forgets the lease and lets the graph be placed again. A node also gives up and retakes its lease each time it reloads a graph, so a late commit from its own old engine instance is refused too.
The fault test stale_owner_append_is_fenced checks this: it holds the old owner's append while another node takes the lease, releases it, and expects the metastore to refuse it and the old owner to start forwarding.
Calls that reach a node that is not the owner are forwarded: Execute, ExecuteBatch, ExecuteStream, Explain and GetSchema go to the owner over an internal route. A forwarded call is never forwarded again. If ownership moved in the meantime, the node answers UNAVAILABLE with graph_reloading and the client retries. A forward waits at most the statement's deadline plus 5 s.
Why a single writer: the decision log's reason for D744 is that "single-writer order makes the log a total order and replay deterministic". With one writer, the stream's offsets are the graph's history, and replaying them reproduces the graph exactly.
Snapshots and the graph manifest
Replaying the whole log on every restart would get slower as the graph ages, so the owner checkpoints. By default it checkpoints every 15 minutes or after 64 MiB of log, whichever comes first. Two kinds of snapshot are written under snap/<offset>/ in the graph's bucket prefix:
- native: Grafeo's own full backup, fast to load;
- portable: nodes and edges as Parquet files (
nodes/part-<n>.parquet,edges/part-<n>.parquet, 65,536 rows per file) with aloams.graph.portablemanifest. They are written every 7 days by default, so a graph can be rebuilt without the same Grafeo version.
A graph manifest (manifest/<ULID>.json) lists the snapshots, each with its log offset, lease epoch, engine version, node and edge counts and checksums, plus the log trim offset. A new manifest is committed by a fenced compare-and-set of the metastore pointer graph-manifest/<gr_id>. The log is never trimmed past the oldest snapshot the manifest keeps, and the trim point also stays behind the newest portable snapshot, so an engine upgrade can always replay from one (ruling R13.3). The policy keeps at most 48 snapshots and a 7-day point-in-time window. These settings are server config only: they have no CLI flags.
Recovery, rehydration and eviction
When a node opens a graph after a crash, a move or an eviction, it picks a base in this order (ruling R14.1):
- the local cache, only if a clean close left its stamp and the CRC-32C and other checks pass;
- otherwise the newest usable snapshot in the bucket, native first, then portable;
- otherwise an empty graph at offset 0, and a replay of the whole stream.
It then replays every change set after the base's offset and starts serving. After a crash, a reload or a fence, it never trusts the local files.
A graph that has been idle for an hour (idle_after, default 1 h) is evicted. Under memory pressure, graphs are also evicted least recently used first. Eviction checkpoints, closes with a clean stamp, gives up the lease and reports GRAPH_STATE_EVICTED. The first statement after that rehydrates the graph on whichever node it lands on.
The durability fault gate (GR1 Task 16) kills the owner at each step: the append, the checkpoint upload, the manifest compare-and-set and the log trim. It checks that no acknowledged write is lost and that no client ever saw a value that was later lost.
Reads and consistency
Strong reads go to the owner. Followers can serve eventual and at_least reads, using consistency tokens in the same shape as the rest of Loams, taken over the graph's stream (D744; GR1 Task 15). An at_least read waits at most 5 s (consistency_wait) for its replica to apply the token, then answers UNAVAILABLE with consistency_wait_timeout.
Key decisions
The graph direction changed twice in three weeks. The decisions index has the full log. These are the rulings that shaped what ships.
| Decision | Ruling | Reason given | Rejected |
|---|---|---|---|
| D42 (2026-09-25) | Narrow the protocols Loams speaks: "Neo4j Bolt/Cypher is dropped" | "Emulating five products' protocols is a five-product trap" | Neo4j emulation |
| D44 (2026-09-25) | "Graph is native, without Cypher or Bolt": mapped graphs over collections, CSR/CSC sidecars in the hot tier, a 1 to 2 hop ExpandExec operator, SQL table functions and an expand stage in hybrid search | "GraphRAG needs vector/BM25 seeds → 1–2 hops → rerank, in one planned query; Cypher parsing and recursive execution have a fraction of the demand of vector/search" | Cypher-first native graphs, Bolt, a Cypher subset, APOC shims, the openCypher TCK and LDBC gates |
| D350 (2026-10-01) | "Graph stays native"; Grafeo is not adopted | Grafeo was young: one maintainer in practice, local storage only, no S3 | Grafeo as the engine |
| D634 (2026-10-04) | The graph surface is GQL (ISO/IEC 39075) over Connect-RPC, with Grafeo embedded as the reference engine; "No Bolt anywhere" | GQL is the ISO standard and is portable across engines; Grafeo is Rust, Apache-2.0, and runs in process with no second wire protocol | Cypher as the surface, Bolt |
| D742 (2026-10-08) | "The graph's truth is its log and the bucket, not Grafeo's files" | Grafeo's WAL defaults to periodic fsync and never reaches the bucket | Grafeo's files as the durable copy |
| D743 (2026-10-08, approved 2026-10-16) | A graph is owned (written with GQL) or linked (written by collection and stream links); the sidecar engine is deferred. "Amends D44's implementation, not its API" | One engine and one API | D44's CSR/CSC sidecars and ExpandExec, for now |
| D744 (2026-10-08) | One fenced writer per graph: a metastore lease with an epoch, appends refused for a stale epoch, strong reads on the owner | "Single-writer order makes the log a total order and replay deterministic" | Multiple writers per graph |
| D748 (2026-10-08, proposed) | Cypher as an opt-in dialect per graph (openCypher 9), off by default; still no Bolt and no APOC | Frameworks such as LangChain and LlamaIndex generate Cypher | Bolt, APOC, procedure shims |
| D759 (2026-10-08) | A Grafeo dependency policy: an exact pin, and a trigger for forking | Grafeo has a single main author | Tracking upstream loosely |
Why the direction changed from D44. D44 bet that GraphRAG needs only short expansions from search seeds, so it planned no graph database: graphs would be read-only projections of collections, traversed by SQL operators. Then the owner asked for a production-ready Grafeo-based graph database (D740). Design 48 resolves the conflict like this. It keeps D44's API: the SQL graph_* table functions, the expand stage and the adapter shapes. It defers D44's engine: the sidecars and ExpandExec. It turns D350's objections into requirements. Grafeo's local-files-only storage became D742: the log and the bucket are the truth. Its own vector index became D749: Grafeo is built without it, so Loams keeps one vector store. Its single-maintainer risk became D759, and then the fork. Some of D44 still stands: no Bolt, and no Cypher by default.
The fork. On 2026-10-12 the owner decided to build Grafeo from Loams' fork, ostrium-labs/grafeo. The fork is kept as a small patch queue on upstream v0.5.43. Its patches fix the bugs GR1 had been working around: a commit listener, timeouts on parameterised statements, ORDER BY on relationships, DETACH DELETE, and epoch continuity. The release tag is v0.5.43-loams.1, pinned by commit in the workspace Cargo.toml. Each patch is meant to be upstreamable.
Agent memory is next, not now. Design 52 proposes that Loams Graph's main product use is agent memory: per-user and per-project memory graphs, with collections as the truth and a linked graph as their projection, bi-temporal facts and code graphs for review, served as loams.memory.v1. It writes no code, and its plan, MM1, has not started. Treat it as direction. It also repeats "no Bolt endpoint" (D891).
What is compatible
- Query language: GQL, profile
gql-1. No Cypher, Gremlin, SPARQL, GraphQL or SQL/PGQ. - Wire: Connect over HTTP on the main port, with JSON or binary protobuf. Generated bindings for
loams.graph.v1come with the Loams Python and TypeScript SDKs, and Go bindings are generated from the same protos. A hand-writtenloams.graphclient facade is planned (D758, GR1 Task 36). - Console: the Graph page in the web console and Loams Desktop (
web/plugins/graph) has a GQL editor, a results table, a graph view, the schema and the graph list. Its JSON contract is fixed byconformance/graph/desktop. - Connectors: a Grafeo sink and source over
loams.graph.v1(ExecuteBatchof parameterised GQL to write,ExecuteStreamwith a cursor to read). - Neo4j drivers, Bolt, Cypher tooling: not supported. No third-party graph client is tested against Loams Graph. The only pinned third-party versions in the graph tests are the reference libraries the algorithm results are checked against: networkx 3.4.2 and graspologic-native 1.3.1.
Compared with the alternatives
| Loams Graph | Neo4j | Memgraph | |
|---|---|---|---|
| Query language | GQL (ISO/IEC 39075), profile gql-1 | Cypher | Cypher |
| Wire protocol | Connect (HTTP) | Bolt, plus HTTP APIs | Bolt |
| Drivers and tools | Loams SDK bindings, the Loams console | Official drivers for many languages, Neo4j Browser and Bloom, APOC, Graph Data Science | Bolt-compatible, so many Neo4j drivers work |
| Durability | Every write in the Loams log before ack; snapshots in your object store | Its own transaction log and store files on disk | In memory, with snapshots and a write-ahead log on disk |
| Scale | One owner node per graph; no sharding | Clustering in its commercial edition | Replication |
| Maturity | Available in 0.1.0; wire package unstable | In production use for over a decade | Years in production |
| Runs with your other data | Same engine, namespaces and auth as Loams collections, streams and search | A separate database | A separate database |
Neo4j is far ahead on everything around the engine: Cypher is the most widely used graph query language, Bolt drivers exist for most languages, and APOC and Graph Data Science cover procedures and algorithms Loams Graph does not have. If your stack already generates Cypher, Neo4j or Memgraph will work today, and Loams Graph will not until the Cypher dialect (GR1 Task 33) lands, and even then without Bolt.
Loams Graph is the better choice when the graph belongs with data that already lives in Loams. A graph can be linked to collections and streams instead of copied by a pipeline. It shares the engine's namespaces, auth and consistency tokens. And its durable state is the bucket and the log you already operate, not another set of disks.
What is missing today
From the GR1 plan, open tasks:
- Quotas and memory governance (Tasks 25, 26): per-graph limits and a node memory default.
- Observability and audit (Task 27).
- HA (Task 28): standbys, fast failover, rolling upgrades. Today a graph recovers from the bucket and the log on another node, but no warm standby is waiting.
- Point-in-time restore, export and import (Task 29). The RPCs are declared in the proto.
- Deployment, dashboards, alerts and runbooks (Task 30); security hardening, an external review and fuzzing (Task 31). DDL stays refused until then.
gql-1written down with its corpus (Task 32), the Cypher dialect and an openCypher TCK subset (Task 33), LDBC-derived performance gates (Task 34), a GraphRAG benchmark (Task 35).- SDK facades and framework adapters for LangChain, LlamaIndex and LightRAG (Tasks 36, 37), and the production exit (Task 39).
- A
loams graphCLI command (CL1 plan, Task 15). - Multi-node graph clusters beyond one host, which wait on the multi-tenant work.
- Loams Memory (design 52, plan MM1).
Try it
The release loams image already includes Loams Graph, so a stack started from the 0.1.0 release guide serves loams.graph.v1 on the engine's main port.
To build it from source:
# Loams Graph (`loams.graph.v1`), which builds the embedded Grafeo engine.
cargo build --release -p loams --locked --features graphTo run a local engine with the graph role and try the GraphRAG example (the example, linked graphs and Search.expand are on dev):
cargo run -p loams --features graph -- dev
python3 examples/graphrag/graphrag.py demo # index corpus/ and answer questions.json
python3 examples/graphrag/graphrag.py ask "Who leads Wren?"A minimal owned graph, as the upgrade fixtures create it. These are the JSON bodies for loams.graph.v1.GraphAdminService/CreateGraph and then loams.graph.v1.GraphService/Execute, in a namespace that exists:
{"namespace": "<ns>", "name": "social", "idempotencyKey": "social-1"}{"namespace": "<ns>", "graph": "social",
"statement": "INSERT (:Person {name: 'Ada'})-[:KNOWS]->(:Person {name: 'Bob'})"}{"namespace": "<ns>", "graph": "social",
"statement": "MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a.name, b.name ORDER BY a.name"}Source
- Crate:
crates/loams-graph. It containsdurable/(the write lane),owner.rs(the fenced lease),snapshot/(checkpoints and the manifest),recovery.rs,evict.rs,service/(the Connect services),link/andretrieval/. - API:
proto/loams/graph/v1/graph.proto. - Conformance:
conformance/graph. - Example:
examples/graphrag. - Design: 48, Loams Graph in production; 07, Graph (native GraphRAG); 52, Loams Memory.
- Plans: GR1, Loams Graph in production; the graph database spike; MM1, Loams Memory.
- Engine fork:
ostrium-labs/grafeo, tagv0.5.43-loams.1.