Skip to content

Tutorial ​

This tutorial walks through building a social graph application with age-orm. Every code snippet here has been tested against a real Apache AGE database.

Setup ​

python
from age_orm import Database, Vertex, Edge, listen, listens_for

Define Models ​

First, define your vertex and edge types:

python
class Person(Vertex):
    __label__ = "Person"
    name: str
    age: int
    email: str | None = None

class Company(Vertex):
    __label__ = "Company"
    name: str
    industry: str

class Knows(Edge):
    __label__ = "KNOWS"
    since: int
    relationship_type: str = "friend"

class WorksAt(Edge):
    __label__ = "WORKS_AT"
    role: str
    start_year: int

The __label__ class variable sets the AGE label. If omitted, the class name is used.

Connect to the Database ​

python
db = Database("postgresql://user:password@localhost/mydb")
graph = db.graph("social", create=True)

The create=True flag creates the graph if it doesn't exist. Without it, GraphNotFoundError is raised for missing graphs.

You can also check graph existence:

python
db.graph_exists("social")  # True
db.list_graphs()           # ['social']

Create Vertices ​

Use graph.add() to create vertices:

python
alice = Person(name="Alice", age=30, email="alice@example.com")
graph.add(alice)

# graph_id is assigned automatically
print(alice.graph_id)  # e.g., 844424930131969
print(alice.is_dirty)  # False -- saved to database

Add more people:

python
bob = Person(name="Bob", age=25)
charlie = Person(name="Charlie", age=35, email="charlie@example.com")
diana = Person(name="Diana", age=28)

for person in [bob, charlie, diana]:
    graph.add(person)

Create Edges ​

Use graph.connect() to create edges between vertices:

python
# Alice knows Bob (colleague since 2020)
graph.connect(alice, Knows(since=2020, relationship_type="colleague"), bob)

# Alice knows Charlie (friend since 2019)
graph.connect(alice, Knows(since=2019, relationship_type="friend"), charlie)

# Bob knows Diana
graph.connect(bob, Knows(since=2021, relationship_type="friend"), diana)

Add a company and employment edge:

python
acme = Company(name="Acme Corp", industry="Technology")
graph.add(acme)

graph.connect(alice, WorksAt(role="Engineer", start_year=2018), acme)

Query with the Query Builder ​

Basic Queries ​

python
# Get all people
everyone = graph.query(Person).all()

# Count
count = graph.query(Person).count()  # 4

# Get one by name
alice = graph.query(Person).filter_by(name="Alice").one()

Filtering ​

filter_by() for equality conditions:

python
alice = graph.query(Person).filter_by(name="Alice").one()

filter() for arbitrary Cypher conditions with $param placeholders:

python
older_people = (
    graph.query(Person)
    .filter("n.age > $min_age", min_age=28)
    .all()
)
# Returns Alice (30), Charlie (35)

Sorting and Pagination ​

python
# Sort by age ascending, get first 2
youngest = (
    graph.query(Person)
    .sort("n.age")
    .limit(2)
    .all()
)
# [Bob (25), Diana (28)]

# Sort descending
oldest = (
    graph.query(Person)
    .sort("n.age DESC")
    .limit(1)
    .all()
)
# [Charlie (35)]

# Skip + Limit for pagination
page_2 = (
    graph.query(Person)
    .sort("n.age")
    .limit(2, skip=2)
    .all()
)

Convenience Lookups ​

python
# By property value
alice = graph.query(Person).by_property("name", "Alice")

# first() returns None instead of raising
nobody = graph.query(Person).filter_by(name="NoSuchPerson").first()
# None

Update Entities ​

Modify fields and call graph.update():

python
alice = graph.query(Person).filter_by(name="Alice").one()
alice.age = 31
alice.email = "alice@newdomain.com"

graph.update(alice)  # Sends all fields

Or update only changed fields:

python
alice.email = "alice@latest.com"
graph.update(alice, only_dirty=True)  # Only sends email

Delete Entities ​

python
# Delete a vertex (DETACH DELETE removes connected edges too)
diana = graph.query(Person).filter_by(name="Diana").one()
graph.delete(diana)
print(diana.graph_id)  # None

Traverse the Graph ​

graph.traverse() follows edges from a vertex. Results are automatically converted to model instances when the vertex/edge label matches a defined model class:

python
# Who does Alice know? (outbound KNOWS edges)
alice_friends = graph.traverse(alice, "KNOWS", direction="outbound")
# [Person(name='Bob', age=25), Person(name='Charlie', age=35)]

# Who knows Bob? (inbound KNOWS edges)
who_knows_bob = graph.traverse(bob, "KNOWS", direction="inbound")
# [Person(name='Alice', age=30)]

You can also pass target_class to force hydration into a specific model:

python
friends = graph.traverse(
    alice, "KNOWS",
    direction="outbound",
    target_class=Person
)

If a result's label doesn't match any defined model class, it falls back to a raw dict.

Raw Cypher Queries ​

For complex queries, use graph.cypher(). Results that are vertices or edges are automatically converted to model instances when their label matches a defined model class.

When you pass columns=, results are returned as dicts keyed by your column names (instead of generic col_0, col_1 keys), and scalar values like {"value": 3} are automatically unwrapped:

python
# Vertex results are auto-hydrated into model instances
results = graph.cypher(
    "MATCH (n:Person) WHERE n.name = $name RETURN n",
    name="Alice"
)
alice = results[0]  # Person(name='Alice', age=30)
print(alice.name)   # "Alice"

# Scalar results with named columns
results = graph.cypher(
    "MATCH (n:Person) RETURN count(n)",
    columns=["total"]
)
total = results[0]["total"]  # 3

# Multi-column results with named keys and auto-hydration
results = graph.cypher(
    "MATCH (a:Person)-[e:KNOWS]->(b:Person) RETURN a, e, b",
    columns=["a", "e", "b"]
)
row = results[0]
row["a"]  # Person instance
row["e"]  # Knows instance
row["b"]  # Person instance

# Scalar columns are unwrapped automatically
results = graph.cypher(
    "MATCH (n:Person) RETURN n.name, n.age",
    columns=["name", "age"]
)
results[0]  # {"name": "Alice", "age": 30}

Column names are automatically quoted in SQL, so reserved words like count, order, etc. work safely as column names.

Bulk Operations ​

For loading large datasets, use bulk insert (bypasses Cypher for speed):

python
people = [
    Person(name="Eve", age=22),
    Person(name="Frank", age=40),
    Person(name="Grace", age=33),
]
graph.bulk_add(people)

# All have graph_ids now
for p in people:
    print(f"{p.name}: {p.graph_id}")

Bulk edge insert:

python
triples = [
    (eve, Knows(since=2022, relationship_type="friend"), frank),
    (frank, Knows(since=2023, relationship_type="colleague"), grace),
]
graph.bulk_add_edges(triples)

Bulk Mutations via Query Builder ​

Update or delete multiple entities at once:

python
# Update all people older than 35
updated = (
    graph.query(Person)
    .filter("n.age > $age", age=35)
    .update(email="senior@example.com")
)
print(f"Updated {updated} records")

# Delete specific people
deleted = (
    graph.query(Person)
    .filter("n.name IN $names", names=["Eve", "Frank", "Grace"])
    .delete()
)
print(f"Deleted {deleted} records")

Events ​

Register hooks for graph operations:

python
@listens_for(Person, ["pre_add", "post_add"])
def on_person_add(target, event, **kwargs):
    print(f"Event: {event} for {target.name}")

person = Person(name="EventTest", age=99)
graph.add(person)
# Prints:
#   Event: pre_add for EventTest
#   Event: post_add for EventTest

See the Events page for more details.

Cleanup ​

python
db.drop_graph("social")
db.close()

Next Steps ​

Building the Apache AGE ecosystem