Cookbooks
Graph Exploration
After 50 memories, you have more than a list — you have a navigable relationship graph. Decisions that led to patterns. Patterns that caused bugs. Bugs that produced conventions. The graph endpoints let you navigate those connections directly.
Problem
Search returns individual memories. But understanding a domain requires understanding how things connect. Why does the workspace isolation convention exist? What chain of decisions led to the current session architecture? Which memories cluster around "database performance"? Graph exploration answers questions that flat search can't.
Persona
Sanjay has 80+ memories in his workspace after four months of active use. He understands individual decisions well, but wants to understand the overall shape of his project's knowledge — what the major clusters are, how key architectural decisions connect, and whether there are any isolated memories that should be linked to the main graph.
Prerequisites
- Neuroloom API key and workspace with 10+ stored memories
- Environment variables set:
export MEMORIES_API_TOKEN="nl_your_api_key_here" export MEMORIES_WORKSPACE_ID="ws_your_workspace_id_here"
Step 1: Explore a topic subgraph
The /memories/explore endpoint returns a subgraph of memories connected to a query topic. It uses semantic similarity to seed the graph, then expands outward along relationship edges.
Use memory_explore with query "database performance" and max_nodes 15curl -X POST https://api.neuroloom.dev/api/v1/memories/explore \
-H "Authorization: Token $MEMORIES_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "'"$MEMORIES_WORKSPACE_ID"'",
"query": "database performance",
"max_nodes": 15,
"relationship_types": ["references", "related_to", "caused_by", "supersedes"],
"min_edge_confidence": 0.6
}'import os
import httpx
token = os.environ["MEMORIES_API_TOKEN"]
workspace_id = os.environ["MEMORIES_WORKSPACE_ID"]
response = httpx.post(
"https://api.neuroloom.dev/api/v1/memories/explore",
headers={"Authorization": f"Token {token}"},
json={
"workspace_id": workspace_id,
"query": "database performance",
"max_nodes": 15,
"relationship_types": ["references", "related_to", "caused_by", "supersedes"],
"min_edge_confidence": 0.6,
},
)
graph = response.json()
print(f"Nodes: {len(graph['nodes'])}")
print(f"Edges: {len(graph['edges'])}")
for node in graph["nodes"]:
print(f" [{node['memory_type']}] {node['title']} (pagerank: {node.get('pagerank_score', 0):.3f})")Response:
{
"nodes": [
{
"id": "mem-6a3f8e1c",
"title": "pgvector ef_search performance ceiling",
"memory_type": "architecture",
"pagerank_score": 0.124,
"community_label": "database-performance"
},
{
"id": "mem-4b7c2d9e",
"title": "HNSW index parameters for production",
"memory_type": "decision",
"pagerank_score": 0.098,
"community_label": "database-performance"
},
{
"id": "mem-8f1a5b3c",
"title": "Workspace isolation invariant",
"memory_type": "convention",
"pagerank_score": 0.187,
"community_label": "database-performance"
},
{
"id": "mem-2e7b9f3d",
"title": "StrEnum over Postgres ENUM for choice fields",
"memory_type": "decision",
"pagerank_score": 0.076,
"community_label": "migrations"
}
],
"edges": [
{
"source": "mem-6a3f8e1c",
"target": "mem-4b7c2d9e",
"relationship_type": "references",
"confidence": 0.89
},
{
"source": "mem-8f1a5b3c",
"target": "mem-6a3f8e1c",
"relationship_type": "references",
"confidence": 0.72
}
]
}The pagerank_score reflects each memory's centrality in the relationship graph — how many other memories reference or connect to it. community_label groups memories that cluster together topically.
The workspace isolation invariant has the highest pagerank (0.187) — it's referenced by more memories than any other node in this subgraph. That's a signal: it's a load-bearing piece of knowledge.
Step 2: Examine nodes and edges
Get the full detail for the highest-pagerank node to understand why it's so central:
Use memory_get_detail with memory_id "mem-8f1a5b3c" and include_related truecurl "https://api.neuroloom.dev/api/v1/memories/mem-8f1a5b3c" \
-H "Authorization: Token $MEMORIES_API_TOKEN"Response:
{
"id": "mem-8f1a5b3c",
"title": "Workspace isolation invariant",
"memory_type": "convention",
"narrative": "Every database query must filter by workspace_id. pgvector similarity queries do not enforce tenant isolation — the WHERE clause is mandatory.",
"pagerank_score": 0.187,
"community_label": "database-performance",
"relationships": [
{
"target_id": "mem-6a3f8e1c",
"relationship_type": "references",
"confidence": 0.72,
"direction": "outbound"
},
{
"source_id": "mem-3c9d1a5f",
"relationship_type": "references",
"confidence": 0.81,
"direction": "inbound"
}
],
"related_memories": [
{
"id": "mem-1b4e7c2a",
"title": "pgvector tenant isolation gap",
"score": 0.88
}
]
}The relationship graph shows this memory is referenced by multiple others — which is why it has high pagerank. The related_memories field surfaces a semantically similar memory that isn't directly linked — a candidate for an explicit relationship.
Step 3: Find the shortest path between two memories
Path queries answer "how did we get from decision A to consequence B?" Use BFS to find the minimum-hop path through the relationship graph.
curl -X POST https://api.neuroloom.dev/api/v1/memories/path \
-H "Authorization: Token $MEMORIES_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "'"$MEMORIES_WORKSPACE_ID"'",
"source_memory_id": "mem-2e7b9f3d",
"target_memory_id": "mem-6a3f8e1c",
"max_depth": 5,
"relationship_types": ["references", "related_to", "caused_by", "supersedes"]
}'response = httpx.post(
"https://api.neuroloom.dev/api/v1/memories/path",
headers={"Authorization": f"Token {token}"},
json={
"workspace_id": workspace_id,
"source_memory_id": "mem-2e7b9f3d", # StrEnum decision
"target_memory_id": "mem-6a3f8e1c", # pgvector performance
"max_depth": 5,
"relationship_types": ["references", "related_to", "caused_by", "supersedes"],
},
)
path = response.json()
if path.get("found"):
print(f"Path length: {len(path['path'])} hops")
for step in path["path"]:
print(f" {step['title']} --[{step.get('edge_type', '')}]--> ")
else:
print("No path found within max_depth")Response:
{
"found": true,
"path": [
{
"id": "mem-2e7b9f3d",
"title": "StrEnum over Postgres ENUM for choice fields",
"memory_type": "decision"
},
{
"id": "mem-9c4d2a7f",
"title": "Zero-downtime migration strategy",
"memory_type": "decision",
"edge_type": "related_to",
"edge_confidence": 0.90
},
{
"id": "mem-8f1a5b3c",
"title": "Workspace isolation invariant",
"memory_type": "convention",
"edge_type": "references",
"edge_confidence": 0.81
},
{
"id": "mem-6a3f8e1c",
"title": "pgvector ef_search performance ceiling",
"memory_type": "architecture",
"edge_type": "references",
"edge_confidence": 0.72
}
],
"depth": 3
}A 3-hop path connects the StrEnum decision to the pgvector performance ceiling through the migration strategy and workspace isolation invariant. This chain tells a story: the decision to avoid Postgres ENUMs shaped the migration approach, which led to the workspace isolation requirement, which informed the pgvector query design.
Step 4: Discover community clusters
The community_label field groups memories that are topically cohesive. Use it to discover the major knowledge clusters in your workspace:
# Get a broad sample of memories
response = httpx.get(
"https://api.neuroloom.dev/api/v1/memories/",
headers={"Authorization": f"Token {token}"},
params={"workspace_id": workspace_id, "limit": 100},
)
memories = response.json()["items"]
# Group by community
from collections import defaultdict
communities: dict[str, list[str]] = defaultdict(list)
for m in memories:
label = m.get("community_label") or "unclustered"
communities[label].append(m["title"])
for label, titles in sorted(communities.items(), key=lambda x: -len(x[1])):
print(f"\n{label} ({len(titles)} memories)")
for title in titles[:5]:
print(f" - {title}")
if len(titles) > 5:
print(f" ... and {len(titles) - 5} more")Output:
database-performance (18 memories)
- pgvector ef_search performance ceiling
- HNSW index parameters for production
- Workspace isolation invariant
- SQLAlchemy lazy loading strategy
- Connection pool sizing
... and 13 more
session-lifecycle (12 memories)
- Session start context injection
- Session end extraction job
- ARQ worker argument serialization
- Background job retry policy
... and 8 more
migrations (9 memories)
- StrEnum over Postgres ENUM for choice fields
- Zero-downtime migration strategy
- Alembic version branching
... and 6 more
unclustered (7 memories)
- API rate limiting approach
- Frontend auth token refresh
...The unclustered group is worth attention — these memories haven't been connected to the main graph yet. They're either genuinely isolated topics or they need explicit relationships added.
Step 5: Use graph insights
Act on what the exploration reveals:
# Find unclustered memories with low pagerank — candidates for archiving or relationship building
low_connectivity = [
m for m in memories
if m.get("pagerank_score", 0) < 0.02
and m.get("community_label") is None
]
print(f"Found {len(low_connectivity)} low-connectivity memories")
for m in low_connectivity[:10]:
print(f" {m['id']} {m['title']}")
print(f" pagerank: {m.get('pagerank_score', 0):.4f} type: {m['memory_type']}")For each low-connectivity memory, either:
- Search for related memories and add explicit relationships
- Update the narrative with more concept keywords to improve automatic relationship discovery
- Archive it if it's genuinely no longer relevant
Production Patterns
Schedule PageRank and community updates
pagerank_score and community_label are computed by background jobs. They don't update in real time — new memories initially have pagerank_score: 0.0 and community_label: null.
Run the graph recompute job periodically (daily for active workspaces, weekly for stable ones). The job is triggered by the ARQ cron schedule in the API. If you have API access to trigger it manually:
curl -X POST https://api.neuroloom.dev/api/v1/workspaces/$MEMORIES_WORKSPACE_ID/graph/recompute \
-H "Authorization: Token $MEMORIES_API_TOKEN"After recompute, all memories get updated pagerank_score and community_label values.
Interpret pagerank_score
pagerank_score reflects graph centrality, not importance or quality. A high pagerank means many other memories reference this one — either explicitly (you created the relationship) or through automatic discovery (embedding similarity above threshold).
Use pagerank to identify knowledge hubs — memories that are worth keeping highly available in session context. Memories with pagerank_score > 0.1 are strong candidates for importance_score: 1.0.
Use community_label for workspace health checks
A healthy workspace has 3–8 major clusters. If everything falls into one giant cluster, your concept labeling is too broad. If you have 20 clusters with 2 memories each, your workspace is fragmented — either add more relationships or consolidate related topics.
Use the cluster distribution as a periodic audit of knowledge quality.
Before You Ship
- Verify
pagerank_scoreandcommunity_labelare populated — trigger a graph recompute if they're null - Explore your 3 most important knowledge domains and confirm the right memories appear in each subgraph
- Run at least one path query between two non-obvious memories — verify the path makes sense
- Review the
unclusteredgroup and either add relationships or archive stale memories - Identify your highest-pagerank memories and confirm they have
importance_score >= 0.9 - Schedule regular graph recompute jobs so scores stay current
Related
- Decision Tracking — build the relationship graph by storing linked decisions
- Multi-Agent Workflows — understand how cross-agent memories connect in the graph
- Concepts: Relationships — relationship types and automatic discovery heuristics
- REST API Reference — full endpoint documentation for explore and path