Products
Available

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).

PieceSince
loams.graph.v1: graph admin and GQL Execute, ExecuteBatch, ExecuteStream, Explain (GR1a)preview.1Available
Writes in the Loams log before acknowledgement (GR1 Task 11)preview.1Available
One fenced owner per graph, forwarding (Task 12); snapshots and the graph manifest (Task 13); recovery, rehydration, idle eviction (Task 14)preview.2Available
Loams' Grafeo fork, ostrium-labs/grafeo v0.5.43-loams.1preview.2Available
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 plannedExperimental
Quotas, metrics, standbys and failover, point-in-time restore, the Cypher dialect, SDK facades (GR1 Tasks 25 to 39)GR1 planPlanned
Agent memory on graphs, Loams Memory (design 52, plan MM1)design onlyPlanned

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

One owner per graph. Any node accepts a call and forwards it to the owner. The owner holds a metastore lease with an epoch and appends to the graph's stream under it. An append from a node that has lost the lease is refused.

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, plus RestoreGraph, ExportGraph and ImportGraph, which are declared but are open work in GR1 Task 29.
  • GraphService: Execute, ExecuteBatch, ExecuteStream (server streaming) and Explain.

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:

  1. A write transaction runs in Grafeo under a per-graph gate.
  2. At commit, its changes are captured as one GraphChangeSet, a hand-encoded, engine-neutral record (format in conformance/graph/records/v1).
  3. 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.
  4. 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 a loams.graph.portable manifest. 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

Recovery (GR1 Task 14). Choose the newest trustworthy base, then replay the stream after it. Here the base is a snapshot at offset 9, and offsets 9 to 14 are replayed.

When a node opens a graph after a crash, a move or an eviction, it picks a base in this order (ruling R14.1):

  1. the local cache, only if a clean close left its stamp and the CRC-32C and other checks pass;
  2. otherwise the newest usable snapshot in the bucket, native first, then portable;
  3. 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.

DecisionRulingReason givenRejected
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 adoptedGrafeo was young: one maintainer in practice, local storage only, no S3Grafeo 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 protocolCypher 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 bucketGrafeo'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 APID44'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 APOCFrameworks such as LangChain and LlamaIndex generate CypherBolt, APOC, procedure shims
D759 (2026-10-08)A Grafeo dependency policy: an exact pin, and a trigger for forkingGrafeo has a single main authorTracking 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.v1 come with the Loams Python and TypeScript SDKs, and Go bindings are generated from the same protos. A hand-written loams.graph client 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 by conformance/graph/desktop.
  • Connectors: a Grafeo sink and source over loams.graph.v1 (ExecuteBatch of parameterised GQL to write, ExecuteStream with 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 GraphNeo4jMemgraph
Query languageGQL (ISO/IEC 39075), profile gql-1CypherCypher
Wire protocolConnect (HTTP)Bolt, plus HTTP APIsBolt
Drivers and toolsLoams SDK bindings, the Loams consoleOfficial drivers for many languages, Neo4j Browser and Bloom, APOC, Graph Data ScienceBolt-compatible, so many Neo4j drivers work
DurabilityEvery write in the Loams log before ack; snapshots in your object storeIts own transaction log and store files on diskIn memory, with snapshots and a write-ahead log on disk
ScaleOne owner node per graph; no shardingClustering in its commercial editionReplication
MaturityAvailable in 0.1.0; wire package unstableIn production use for over a decadeYears in production
Runs with your other dataSame engine, namespaces and auth as Loams collections, streams and searchA separate databaseA 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-1 written 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 graph CLI 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 graph

To 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

On this page