Python graph OGM
Map Python classes to graph nodes and edges. Typed fields, indexes, constraints, lazy/eager relationships, and a fluent query builder that compiles to Cypher.
Map Python classes to graph nodes and edges, version your schema like Alembic, and ground LLM answers in a knowledge graph — on FalkorDB, ArcadeDB, Neo4j, Memgraph, Apache AGE or Amazon Neptune.
runic is a Python graph OGM — an object-graph mapper for Cypher graph databases. You declare typed Node and Edge classes, and runic maps them to labels, relationship types, indexes and constraints, tracks your changes, and compiles a fluent query API down to Cypher. The same model code runs unchanged on FalkorDB, ArcadeDB, Neo4j, Memgraph, Apache AGE and Amazon Neptune (the PostgreSQL graph extension).
Two more layers ship in the same Python package:
uv add "runic-py[falkordb]" # or: pip install "runic-py[falkordb]"runic requires Python 3.14+, is MIT-licensed, and is published on PyPI as runic-py.
Map classes to nodes, write a relationship, then traverse it with the fluent query builder — no raw Cypher:
from runic.ogm import (
Field,
Node,
Relation,
Session,
alias,
create_driver,
select,
)
class Person(Node, labels=["Person"]):
id: str = Field(primary_key=True)
name: str
email: str = Field(unique=True)
friends: list["Person"] = Relation(
relationship="FRIENDS",
direction="OUTGOING",
target="Person",
)
driver = create_driver("falkordb", host="localhost", port=6379, graph="myapp")
with Session(driver) as session:
alice = Person(id="alice", name="Alice", email="alice@example.com")
bob = Person(id="bob", name="Bob", email="bob@example.com")
session.add(alice)
session.add(bob)
session.relate(alice, Person.friends, bob)
session.commit()
# Traverse the social graph: everyone Alice is friends with
p, f = alias(Person, "p"), alias(Person, "f")
stmt = (
select(p)
.where(p.id == "alice")
.traverse(Person.friends, to=f)
.return_target(f)
)
friends: list[Person] = session.scalars(stmt)
print([person.name for person in friends]) # ['Bob']
# MATCH (p:Person)
# WHERE p.id = $p0
# MATCH (p)-[:FRIENDS]->(f:Person)
# RETURN fTrack graph schema changes as versioned revision scripts, the way Alembic does for SQL. Generate one with runic revision, then describe the change with op.* calls:
# runic/versions/3f9a12c1_add_person_email_index.py
def upgrade(op) -> None:
op.create_range_index("Person", "email")
def downgrade(op) -> None:
op.drop_range_index("Person", "email")Apply and roll back from the CLI:
runic revision -m "add person email index" # scaffold the script above
runic upgrade # apply pending revisions
runic current # 3f9a12c1 — add person email index
runic downgrade base # roll back to an empty schemaTurn unstructured text into a knowledge graph, then ask questions and get answers grounded in cited source chunks — extraction, embedding, storage, and hybrid retrieval handled for you:
uv add "runic-py[graphrag,falkordb]" # Graph-RAG extras + a backend driverfrom runic.ogm import create_driver
from runic.rag import GraphRAG, Ontology, RagSettings
settings = RagSettings()
driver = create_driver(
"falkordb", host="localhost", port=6379, graph=settings.falkordb_graph
)
rag = GraphRAG.with_defaults(driver, settings=settings, ontology=Ontology.default())
rag.bootstrap_schema()
rag.ingest_text(
"Ada Lovelace worked with Charles Babbage on the Analytical Engine.",
source="inline-demo",
)
answer = rag.query("Who worked on the Analytical Engine?")
print(answer.text)
for citation in answer.citations:
print(f" - [{citation.source}] {citation.text[:80]}...")Set OPENAI_API_KEY first (or point runic.rag at Ollama for a fully local run). mode="auto" picks a focused or broad retrieval strategy per question.
One model definition, seven backends. Switching graph database means changing the arguments to create_driver() — your models, queries, and application code stay the same.
| Graph database | Install extra | Notes |
|---|---|---|
| FalkorDB | runic-py[falkordb] | Redis-based; also runs embedded and in-process for tests |
| Neo4j | runic-py[neo4j] | Official Bolt driver |
| Memgraph | runic-py[memgraph] | In-memory, over Bolt |
| ArcadeDB | runic-py[arcadedb] | Multi-model, over Bolt |
| Apache AGE | runic-py[age] | Graphs inside PostgreSQL |
| Amazon Neptune Database | runic-py[neptune] | AWS-managed, over Bolt with IAM auth |
| Amazon Neptune Analytics | runic-py[neptune-analytics] | AWS-managed, HTTPS, native vector search |
See Supported Drivers for connection options and per-backend capability differences.
These snippets barely scratch it. runic is built for the hard parts of real graph work — the things you hit on day two, not day one:
.traverse() calls or use .traverse(..., hops=(min, max)) to walk org charts, dependency trees, and recommendation paths without hand-writing *1..5 Cypher.Edge, read it back with all_with_edges(), and filter on the edge.vector_search() / fulltext_search() — native, not bolted on.await-ed.upgrade/downgrade workflow runs unchanged across FalkorDB, ArcadeDB, Neo4j, Memgraph, Apache AGE, and Amazon Neptune.A graph OGM (object-graph mapper) is to graph databases what an ORM is to relational ones. Instead of writing Cypher and reading back raw rows, you declare typed Python classes; the OGM maps them to nodes, edges, labels and relationship types, tracks changes, and generates the queries. In runic you define models by subclassing Node and Edge and declaring Field and Relation attributes, and a Session handles reads, writes and change tracking.
FalkorDB, Neo4j, Memgraph, ArcadeDB, Apache AGE (the PostgreSQL graph extension) and Amazon Neptune — Database and Analytics. The same model and query code runs on all seven — see Supported Drivers.
Yes. AsyncSession and AsyncRepository mirror the synchronous API call for call, so async code is the same code awaited. There are no hidden lazy loads, so query patterns stay deterministic under concurrency. See the Async Guide.
Graph-RAG grounds LLM answers in a knowledge graph instead of a flat vector index, so retrieval can follow relationships between entities rather than only matching similar text. runic.rag ingests documents by chunking them, extracting entities and relations against an ontology, embedding them, and storing everything as a graph. Retrieval fuses graph traversal and vector search with reciprocal rank fusion, and every answer comes back with citations to its source chunks. Start with What is Graph-RAG?.
Yes. The query builder covers traversals, filtering, aggregation, paging, vector KNN and fulltext search, and every builder call documents the Cypher it compiles to. When you need something the builder does not model, run a raw Cypher statement through the same session and driver.
No. The testing guide uses an embedded, in-process FalkorDB, so unit tests covering CRUD, relationships and queries run without any external server or container. Docker is only needed when you want to test against a specific live backend.
runic requires Python 3.14 or newer and is published on PyPI as runic-py. It is open source under the MIT license, developed at github.com/jenreh/runic.
Start with a quickstart and you'll have something running in five minutes:
Then go deep:
relate(), edge propertiesop.* call at a glanceBring your own backend. Write your models once. runic handles the Cypher.