Quick Start
Get a graph running in five minutes. Choose Python or
Node — both are thin bindings over the same Rust engine. Every query and
analyst verb returns Apache Arrow results. Node and edge handles use stable
.uuid identity (no numeric storage ids).
For full install options, see Installation.
Studio (editor): Prefer working inside VS Code or Cursor? Install GraphForge for VS Code (also on Open VSX) — the editor workflow marketed as Studio on the product site. It detects Python- and Node-first workspaces, configures the matching binding, and runs the same Rust-owned engine. Setup and commands: Studio / VS Code extension guide.
Install
Section titled “Install”Python
Section titled “Python”pip
pip install graphforgeuv (recommended)
uv add graphforgenpm
npm install @curatelabs/graphforgepnpm
pnpm add @curatelabs/graphforgeNode query and analyst-verb results are Arrow IPC buffers. Decode them with
apache-arrow (tableFromIPC)
when you want table helpers in JavaScript.
Create and Query a Graph
Section titled “Create and Query a Graph”Python
Section titled “Python”from graphforge import GraphForge
forge = GraphForge() # in-memory; use GraphForge("my-graph/") for persistence
# Add nodes — returns a NodeHandle (use .uuid for identity)alice = forge.add_node("Person", name="Alice", age=30)bob = forge.add_node("Person", name="Bob", age=25)
# Add a relationshipforge.add_edge(alice, "KNOWS", bob, since=2020)
# Query with openCypher — returns an Arrow Tabletable = forge.execute(""" MATCH (p:Person)-[:KNOWS]->(friend:Person) WHERE p.age > 25 RETURN p.name AS person, friend.name AS friend, p.age AS age ORDER BY p.age DESC""")
# Consume the resultdf = table.to_pandas()print(df)# person friend age# 0 Alice Bob 30forge.execute() always returns a PyArrow Table. Use table.to_pandas() for pandas,
pl.from_arrow(table) for Polars, or iterate rows with table.to_pylist().
import { tableFromIPC } from "apache-arrow";import { GraphForge } from "@curatelabs/graphforge";
const forge = new GraphForge(); // in-memory; use new GraphForge("my-graph/") for persistence
// Add nodes — returns a NodeHandle (use .uuid for identity)const alice = forge.addNode("Person", { name: "Alice", age: 30 });const bob = forge.addNode("Person", { name: "Bob", age: 25 });
// Add a relationshipforge.addEdge(alice, "KNOWS", bob, { since: 2020 });
// Query with openCypher — returns an Arrow IPC bufferconst table = tableFromIPC(forge.execute(` MATCH (p:Person)-[:KNOWS]->(friend:Person) WHERE p.age > 25 RETURN p.name AS person, friend.name AS friend, p.age AS age ORDER BY p.age DESC`));
console.log(table.toArray());// [ { person: 'Alice', friend: 'Bob', age: 30 } ]forge.execute() returns an Arrow IPC buffer. Decode with tableFromIPC(...)
from apache-arrow, then use table.toArray(), column accessors, or other
Arrow JS helpers.
Persist a Graph
Section titled “Persist a Graph”Pass a directory path instead of leaving it empty. GraphForge initializes the project inside that directory, stores the graph as Parquet files, and reloads it automatically on the next open.
The directory must already exist — GraphForge opens a project root, it does not create the
directory for you. Opening a missing path raises StorageError: path does not exist.
Python
Section titled “Python”from pathlib import Pathfrom graphforge import GraphForge
Path("research").mkdir(parents=True, exist_ok=True)
forge = GraphForge("research/")forge.add_node("Paper", title="Graph Neural Networks", year=2024)forge.close()
# Reload in a later session (the directory now exists)forge = GraphForge("research/")table = forge.execute("MATCH (p:Paper) RETURN p.title AS title")print(table.column("title")[0].as_py()) # Graph Neural Networksimport { mkdirSync } from "node:fs";import { tableFromIPC } from "apache-arrow";import { GraphForge } from "@curatelabs/graphforge";
mkdirSync("research", { recursive: true });
let forge = new GraphForge("research/");forge.addNode("Paper", { title: "Graph Neural Networks", year: 2024 });forge.close();
// Reload in a later session (the directory now exists)forge = new GraphForge("research/");const table = tableFromIPC( forge.execute("MATCH (p:Paper) RETURN p.title AS title"),);console.log(table.getChild("title").get(0)); // Graph Neural NetworksBulk Load
Section titled “Bulk Load”For loading many nodes or edges at once, use the atomic Arrow bulk surfaces
(publish_bulk_nodes / publish_bulk_edges in Python;
publishBulkNodes / publishBulkEdges in Node) or the Python convenience helpers
add_nodes() / add_edges(). Pass a stable operation_uuid and receive a
canonical receipt table. Python inputs may be a list of dicts, a pandas DataFrame,
or an Arrow Table. Node bulk publication takes Arrow IPC. See
Graph Construction for the full scalar + bulk path.
The operation identity must be a UUIDv7 — uuid.uuid4() is rejected with
GF_BULK_VALIDATION(invalid_uuid). Python 3.14 ships uuid.uuid7(); on earlier
versions generate one with the stdlib helper below.
Python
Section titled “Python”import osimport pandas as pdimport timeimport uuid
def uuid7() -> uuid.UUID: """RFC 9562 UUIDv7. Use uuid.uuid7() directly on Python 3.14+.""" stamp = int(time.time() * 1000).to_bytes(6, "big") raw = bytearray(stamp + os.urandom(10)) raw[6] = (raw[6] & 0x0F) | 0x70 # version 7 raw[8] = (raw[8] & 0x3F) | 0x80 # RFC 9562 variant return uuid.UUID(bytes=bytes(raw))
node_op = str(uuid7())edge_op = str(uuid7())
# List of dicts → receipt tablenodes = forge.add_nodes( "Paper", [ {"title": "Graph Neural Networks", "year": 2021, "citations": 150}, {"title": "Deep Learning Fundamentals", "year": 2019, "citations": 500}, {"title": "Attention Is All You Need", "year": 2017, "citations": 2000}, ], operation_uuid=node_op,)
# From a DataFrame (endpoint columns renamed to source_uuid/target_uuid)edges_df = pd.DataFrame({ "src_id": [nodes.column("entity_uuid")[0].as_py(), nodes.column("entity_uuid")[1].as_py()], "dst_id": [nodes.column("entity_uuid")[2].as_py(), nodes.column("entity_uuid")[2].as_py()], "weight": [0.8, 0.6],})forge.add_edges("CITES", edges_df, operation_uuid=edge_op, src="src_id", dst="dst_id")Node bulk construction publishes Arrow IPC through publishBulkNodes /
publishBulkEdges with a stable UUIDv7 operationUuid. See
Graph Construction and the Node binding tests for the
IPC table shape.
Rank Nodes
Section titled “Rank Nodes”forge.rank() scores every node of a given label and returns an Arrow result
containing all node properties plus a score column. No mutation happens unless
you pass write_property / the write-back argument.
Python
Section titled “Python”# Read-only — just get the scores back as a tabletable = forge.rank("Person", by="pagerank")df = table.to_pandas()print(df[["name", "score"]].sort_values("score", ascending=False))
# Restrict to a specific relationship typetable = forge.rank("Person", by="betweenness", via="KNOWS", directed=False)
# Opt-in write-back — stores the score as a node propertyforge.rank("Person", by="pagerank", write_property="rank")forge.execute("MATCH (n:Person) RETURN n.name, n.rank ORDER BY n.rank DESC LIMIT 5")import { tableFromIPC } from "apache-arrow";
// Read-only — decode the Arrow IPC bufferconst table = tableFromIPC(forge.rank("Person", "pagerank"));console.log(table.toArray());
// Restrict to a relationship type (via, directed)const between = tableFromIPC( forge.rank("Person", "betweenness", "KNOWS", false),);
// Opt-in write-back — stores the score as a node propertyforge.rank("Person", "pagerank", undefined, true, "rank");forge.execute( "MATCH (n:Person) RETURN n.name, n.rank ORDER BY n.rank DESC LIMIT 5",);Example values for by: pagerank, betweenness, closeness, degree,
clustering_coefficient, triangles. See the
complete canonical catalog.
Find Relevant Content
Section titled “Find Relevant Content”forge.find() runs a hybrid text + vector search and returns an Arrow result with
node properties alongside score and matched_on columns. The index is built
automatically on the first call — no setup step required. label is required on
every call.
Python
Section titled “Python”# Text search — index built lazily on first calltable = forge.find("graph neural networks", label="Paper")df = table.to_pandas()print(df[["title", "score", "matched_on"]])# title score matched_on# 0 Graph Neural Networks 0.924 text# 1 GNN Applications in NLP 0.781 text
# Restrict to a label and limit resultstable = forge.find("graph neural networks", label="Paper", limit=20)
# Hybrid search — pass a vector alongside the text queryimport openaiclient = openai.OpenAI()query_vec = client.embeddings.create( input="graph neural networks", model="text-embedding-3-small").data[0].embedding
table = forge.find("graph neural networks", label="Paper", vector=query_vec)
# Vector-only searchtable = forge.find(vector=query_vec, label="Paper")matched_on is "text", "vector", or "text+vector". GraphForge stores and queries
vectors but does not generate them — bring your own embeddings from any model.
For explicit control over index timing (e.g. batch ingestion before first search):
forge.index("Paper", properties=["title", "abstract"])
# Vector mode takes the node handle itself (node=), not a numeric idforge.index("Paper", node=paper_handle, vector=embedding, space="sbert")import { tableFromIPC } from "apache-arrow";
// Text search — query, label, then optional vector / similarTo / semanticQuery / limitconst table = tableFromIPC(forge.find("graph neural networks", "Paper"));console.log(table.toArray());
// Limit results (positional: query, label, vector, similarTo, semanticQuery, limit)const limited = tableFromIPC( forge.find("graph neural networks", "Paper", undefined, undefined, undefined, 20),);Group into Communities
Section titled “Group into Communities”forge.cluster() assigns every node of a given label to a community and returns an
Arrow result with node properties plus a community_id column.
Python
Section titled “Python”# Read-only community detectiontable = forge.cluster("Person", by="louvain")df = table.to_pandas()print(df.groupby("community_id")["name"].apply(list))
# Restrict to a relationship type and write the result backforge.cluster("Person", by="louvain", via="KNOWS", write_property="community")forge.execute(""" MATCH (n:Person) RETURN n.community AS community, count(*) AS size ORDER BY size DESC LIMIT 5""")import { tableFromIPC } from "apache-arrow";
// Read-only community detectionconst table = tableFromIPC(forge.cluster("Person", "louvain"));console.log(table.toArray());
// Restrict to a relationship type and write the result backforge.cluster("Person", "louvain", "KNOWS", false, "community");forge.execute(` MATCH (n:Person) RETURN n.community AS community, count(*) AS size ORDER BY size DESC LIMIT 5`);Example values for by: louvain, components. See the
complete canonical catalog.
Next Steps
Section titled “Next Steps”- Studio / VS Code extension — explore projects and run Cypher in the editor
- Tutorial — guided walkthrough with a full citation network example
- Graph Construction — scalar API and atomic bulk batches
- Cypher Reference — complete query language documentation
- Analytics Integration — Arrow, pandas, Polars, rank, cluster, find
- API Reference — full Python API
- Datasets (backlog) — planned open-dataset catalogs (not in v0.5.0)