Skip to main content

Graph Operations API

Last Updated: 2025-10-30 Version: v0.7.0+
Status: Production Ready

Complete API reference for graph database integration, multi-hop queries, and knowledge graph operations.

Overview

The Graph Operations API (@cortexmemory/sdk/graph) provides advanced graph database capabilities for Cortex, enabling multi-hop relationship queries, knowledge discovery, and cross-layer context enrichment.

Key Characteristics:

  • Optional - Graph integration is completely optional
  • Multi-Database - Works with Neo4j, Memgraph (single codebase)
  • Real-Time Sync - Reactive worker for automatic synchronization
  • Orphan-Safe - Sophisticated deletion with circular reference protection
  • Cross-Layer - Connects L1a, L2, L3, L4 via relationships
  • Backward Compatible - Existing code works unchanged

When to Use Graph:

  • Deep context chains (5+ levels)
  • Knowledge graphs with entity relationships
  • Multi-hop reasoning requirements
  • Provenance and audit trail needs
  • Complex multi-agent coordination
  • Large-scale fact databases (100s+ facts)

Setup & Configuration

Installation

# Install Neo4j driver
npm install neo4j-driver

# Start graph database (Docker)
docker-compose -f docker-compose.graph.yml up -d neo4j

See Graph Database Setup Guide for complete setup.

Configuration

import { Cortex } from "@cortexmemory/sdk";
import {
CypherGraphAdapter,
initializeGraphSchema,
} from "@cortexmemory/sdk/graph";

// 1. Setup graph adapter
const graphAdapter = new CypherGraphAdapter();
await graphAdapter.connect({
uri: "bolt://localhost:7687",
username: "neo4j",
password: "your-password",
});

// 2. Initialize schema (one-time)
await initializeGraphSchema(graphAdapter);

// 3. Initialize Cortex with graph
const cortex = new Cortex({
convexUrl: process.env.CONVEX_URL!,
graph: {
adapter: graphAdapter,
orphanCleanup: true, // Enable orphan detection (default: true)
autoSync: true, // Auto-start sync worker (default: false)
syncWorkerOptions: {
batchSize: 100,
retryAttempts: 3,
verbose: false,
},
},
});

Core Operations

GraphAdapter

Low-level graph database operations.

connect()

Connect to graph database.

const adapter = new CypherGraphAdapter();
await adapter.connect({
uri: "bolt://localhost:7687",
username: "neo4j",
password: "password",
});

createNode()

Create a node in the graph.

const nodeId = await adapter.createNode({
label: "Memory",
properties: {
memoryId: "mem-123",
content: "User prefers dark mode",
importance: 85,
},
});

createEdge()

Create a relationship between nodes.

const edgeId = await adapter.createEdge({
type: "REFERENCES",
from: memoryNodeId,
to: conversationNodeId,
properties: {
messageIds: ["msg-1", "msg-2"],
createdAt: Date.now(),
},
});

query()

Execute Cypher query.

const result = await adapter.query(
`
MATCH (m:Memory)-[:REFERENCES]->(c:Conversation)
WHERE m.importance >= $minImportance
RETURN m, c
LIMIT 10
`,
{ minImportance: 80 },
);

for (const record of result.records) {
console.log(record.m.properties, record.c.properties);
}

traverse()

Multi-hop graph traversal.

const connected = await adapter.traverse({
startId: nodeId,
relationshipTypes: ["CHILD_OF", "PARENT_OF"],
maxDepth: 5,
direction: "BOTH",
});

console.log(`Found ${connected.length} connected nodes`);

findPath()

Find shortest path between nodes.

const path = await adapter.findPath({
fromId: aliceNodeId,
toId: bobNodeId,
maxHops: 10,
});

if (path) {
console.log(`Path length: ${path.length} hops`);
console.log(`Nodes: ${path.nodes.map((n) => n.label).join(" → ")}`);
console.log(
`Relationships: ${path.relationships.map((r) => r.type).join(" → ")}`,
);
}

Sync Operations

syncToGraph Option

All Cortex APIs now support optional graph synchronization.

Convenience APIs (Auto-Sync)

// memory.remember() auto-syncs by default if graph configured
await cortex.memory.remember({
memorySpaceId: "agent-1",
conversationId: "conv-123",
userMessage: "Alice works at Acme Corp",
agentResponse: "Got it!",
userId: "alice",
userName: "Alice",
});
// ✅ Automatically synced to graph!

// Disable if needed
await cortex.memory.remember(params, { syncToGraph: false });

Low-Level APIs (Manual Sync)

// Explicit opt-in via syncToGraph option
await cortex.vector.store(memorySpaceId, data, {
syncToGraph: true,
});

await cortex.facts.store(params, {
syncToGraph: true,
});

await cortex.contexts.create(params, {
syncToGraph: true,
});

await cortex.conversations.create(input, {
syncToGraph: true,
});

Manual Sync Functions

Direct sync control for power users.

import {
syncMemoryToGraph,
syncFactToGraph,
syncContextToGraph,
syncMemoryRelationships,
syncFactRelationships,
syncContextRelationships,
} from "@cortexmemory/sdk/graph";

// Manual sync workflow
const memory = await cortex.vector.store(memorySpaceId, data);
const nodeId = await syncMemoryToGraph(memory, adapter);
await syncMemoryRelationships(memory, nodeId, adapter);

Real-Time Sync Worker

Automatic synchronization using Convex reactive queries.

Configuration

const cortex = new Cortex({
convexUrl: "...",
graph: {
adapter: graphAdapter,
autoSync: true, // ← Auto-start worker!
syncWorkerOptions: {
batchSize: 100, // Items per batch
retryAttempts: 3, // Retry failures
verbose: true, // Enable logging
},
},
});

Worker Control

// Get worker instance
const worker = cortex.getGraphSyncWorker();

if (worker) {
// Get health metrics
const metrics = worker.getMetrics();
console.log("Worker metrics:", metrics);
/*
{
isRunning: true,
totalProcessed: 150,
successCount: 148,
failureCount: 2,
avgSyncTimeMs: 45,
queueSize: 3,
lastSyncAt: 1635789012345
}
*/
}

// Worker stops automatically when cortex.close() is called
cortex.close();

Manual Worker Control

import { GraphSyncWorker } from "@cortexmemory/sdk/graph";

// Create worker manually
const worker = new GraphSyncWorker(client, graphAdapter, {
batchSize: 50,
verbose: true,
});

// Start worker
await worker.start();
console.log("Worker started, subscribing to sync queue...");

// Use Cortex normally
await cortex.memory.remember(params);
// Worker processes in background reactively!

// Monitor
setInterval(() => {
const metrics = worker.getMetrics();
console.log(
`Processed: ${metrics.totalProcessed}, Queue: ${metrics.queueSize}`,
);
}, 5000);

// Stop worker
worker.stop();

Delete Operations with Orphan Cleanup

Cascading Deletes

All delete operations support sophisticated orphan cleanup:

// Delete memory - checks if conversation becomes orphaned
await cortex.memory.forget("agent-1", "mem-123", {
deleteConversation: true,
syncToGraph: true, // Enables orphan cleanup
});

// What happens:
// 1. Deletes memory from Convex (L2)
// 2. Deletes conversation from Convex (L1a) if requested
// 3. Deletes memory node from graph
// 4. Checks if conversation node is orphaned
// 5. If orphaned, deletes conversation node
// 6. Handles circular references safely
// 7. Detects and removes orphan islands

Orphan Detection

Handles complex scenarios:

// Scenario: Circular references
Entity AKNOWS → Entity B
Entity BKNOWS → Entity A
Fact F1MENTIONS → Entity A

// Delete F1:
// - A and B reference each other (circular!)
// - But no external references remain (F1 was only anchor)
// - Algorithm: Detects orphan island, deletes both A and B ✅

// Scenario: Still referenced
Memory M1 → Conversation C1
Memory M2 → Conversation C1

// Delete M1:
// - C1 still referenced by M2
// - Algorithm: Keeps C1 (not orphaned) ✅

Orphan Rules:

  • Conversation: Deleted if no Memory/Fact/Context references it
  • Entity: Deleted if no Fact mentions it
  • User: Never auto-deleted
  • MemorySpace: Never auto-deleted
  • Memory/Fact/Context: Only deleted if explicitly requested

Schema Management

initializeGraphSchema()

Create constraints and indexes (one-time setup).

import { initializeGraphSchema } from "@cortexmemory/sdk/graph";

await initializeGraphSchema(adapter);
// Creates:
// - 8 unique constraints (MemorySpace, Context, Memory, Fact, etc.)
// - 22 performance indexes

verifyGraphSchema()

Check if schema is properly initialized.

import { verifyGraphSchema } from "@cortexmemory/sdk/graph";

const status = await verifyGraphSchema(adapter);
console.log("Schema valid:", status.valid);
console.log("Constraints:", status.constraints.length);
console.log("Indexes:", status.indexes.length);

dropGraphSchema()

Remove all constraints and indexes (testing/reset).

import { dropGraphSchema } from "@cortexmemory/sdk/graph";

await dropGraphSchema(adapter);
// ⚠️ WARNING: Removes all schema constraints and indexes!

Graph Enrichment Queries

Pattern 1: Memory → Facts Enrichment

// Get memory
const memory = await cortex.vector.get(memorySpaceId, memoryId);

// Find related facts via conversation
const relatedFacts = await adapter.query(
`
MATCH (m:Memory {memoryId: $memoryId})
MATCH (m)-[:REFERENCES]->(conv:Conversation)
MATCH (conv)<-[:EXTRACTED_FROM]-(f:Fact)
RETURN f.fact as fact, f.confidence as confidence
ORDER BY f.confidence DESC
`,
{ memoryId },
);

console.log(
`Memory enrichment: 1 memory → ${relatedFacts.count} related facts`,
);
// Enrichment factor: 5x more context!

Pattern 2: Entity Network Discovery

// Find who works at same company as Alice
const coworkers = await adapter.query(`
MATCH (alice:Entity {name: 'Alice'})-[:WORKS_AT]->(company:Entity)
MATCH (company)<-[:WORKS_AT]-(coworker:Entity)
WHERE coworker.name <> 'Alice'
RETURN DISTINCT coworker.name as name
`);

console.log(
"Alice's coworkers:",
coworkers.records.map((r) => r.name),
);

Pattern 3: Knowledge Path Discovery

// Multi-hop path: Alice → Company → Bob → Technology
const path = await adapter.query(`
MATCH path = (alice:Entity {name: 'Alice'})-[*1..4]-(tech:Entity {name: 'TypeScript'})
RETURN [node in nodes(path) | node.name] as pathNodes,
[rel in relationships(path) | type(rel)] as pathRels,
length(path) as hops
LIMIT 1
`);

if (path.count > 0) {
console.log("Path:", path.records[0].pathNodes.join(" → "));
console.log("Via:", path.records[0].pathRels.join(" → "));
console.log("Hops:", path.records[0].hops);
}

Pattern 4: Context Chain Reconstruction

// Get full context hierarchy via graph
const chain = await adapter.query(
`
MATCH (current:Context {contextId: $contextId})
MATCH path = (current)-[:CHILD_OF*0..10]->(ancestors:Context)
RETURN ancestors
ORDER BY ancestors.depth
`,
{ contextId },
);

console.log("Full context chain:");
for (const record of chain.records) {
const ctx = record.ancestors.properties;
console.log(` Depth ${ctx.depth}: ${ctx.purpose}`);
}

Pattern 5: Provenance Tracing

// Trace fact back to source conversation
const provenance = await adapter.query(
`
MATCH (f:Fact {factId: $factId})
MATCH (f)-[:EXTRACTED_FROM]->(conv:Conversation)
MATCH (conv)<-[:TRIGGERED_BY]-(ctx:Context)
MATCH (ctx)-[:INVOLVES]->(user:User)
RETURN conv.conversationId as conversation,
ctx.purpose as context,
user.userId as user
`,
{ factId },
);

console.log("Fact provenance:");
console.log(" Conversation:", provenance.records[0].conversation);
console.log(" Context:", provenance.records[0].context);
console.log(" User:", provenance.records[0].user);
// Complete audit trail! ✅

Node Types

MemorySpace

(:MemorySpace {
memorySpaceId: string,
name: string,
type: string, // 'personal', 'team', 'project', 'custom'
status: string,
createdAt: number
})

Context

(:Context {
contextId: string,
memorySpaceId: string,
purpose: string,
status: string,
depth: number,
parentId: string,
rootId: string,
createdAt: number
})

Memory

(:Memory {
memoryId: string,
memorySpaceId: string,
content: string, // Truncated (first 200 chars)
importance: number,
sourceType: string,
tags: array,
createdAt: number
})

Fact

(:Fact {
factId: string,
memorySpaceId: string,
fact: string,
factType: string,
subject: string,
predicate: string,
object: string,
confidence: number,
createdAt: number
})

Entity

(:Entity {
name: string,
type: string,
createdAt: number
})

Conversation

(:Conversation {
conversationId: string,
memorySpaceId: string,
type: string,
messageCount: number,
createdAt: number
})

User

(:User {
userId: string,
createdAt: number
})

Relationship Types

Hierarchy

(Context)-[:PARENT_OF]->(Context)
(Context)-[:CHILD_OF]->(Context)

Isolation

(Memory|Fact|Context|Conversation)-[:IN_SPACE]->(MemorySpace)

References

(Memory)-[:REFERENCES]->(Conversation)
(Fact)-[:EXTRACTED_FROM]->(Conversation)
(Context)-[:TRIGGERED_BY]->(Conversation)

Users

(Memory|Context|Conversation)-[:INVOLVES|RELATES_TO]->(User)

Facts & Entities

(Fact)-[:MENTIONS]->(Entity)
(Entity)-[:WORKS_AT|KNOWS|USES|LOCATED_IN|...]->(Entity)

Versioning

(Fact)-[:SUPERSEDES]->(Fact)

syncToGraph Option Reference

All Cortex APIs support the syncToGraph option:

Conversations API

await cortex.conversations.create(input, { syncToGraph: true });
await cortex.conversations.addMessage(input, { syncToGraph: true });
await cortex.conversations.delete(id, { syncToGraph: true });

Vector API

await cortex.vector.store(memorySpaceId, input, { syncToGraph: true });
await cortex.vector.update(memorySpaceId, memoryId, updates, {
syncToGraph: true,
});
await cortex.vector.delete(memorySpaceId, memoryId, { syncToGraph: true });

Facts API

await cortex.facts.store(params, { syncToGraph: true });
await cortex.facts.update(memorySpaceId, factId, updates, {
syncToGraph: true,
});
await cortex.facts.delete(memorySpaceId, factId, { syncToGraph: true });

Contexts API

await cortex.contexts.create(params, { syncToGraph: true });
await cortex.contexts.update(contextId, updates, { syncToGraph: true });
await cortex.contexts.delete(contextId, { syncToGraph: true });

Memory Spaces API

await cortex.memorySpaces.register(params, { syncToGraph: true });

Memory API (Convenience)

// Auto-syncs by default if graph configured
await cortex.memory.remember(params); // syncToGraph: true by default

// Explicit control
await cortex.memory.remember(params, { syncToGraph: false });

// Forget with cascade
await cortex.memory.forget(memorySpaceId, memoryId, {
deleteConversation: true,
syncToGraph: true, // Orphan cleanup enabled
});

Batch Sync

For initial sync or large data imports:

import { initialGraphSync } from "@cortexmemory/sdk/graph";

const result = await initialGraphSync(cortex, adapter, {
limits: {
memorySpaces: 1000,
contexts: 5000,
memories: 10000,
facts: 5000,
},
syncRelationships: true,
onProgress: (entity, current, total) => {
console.log(`Syncing ${entity}: ${current}/${total}`);
},
});

console.log("Sync complete:");
console.log(" Memory Spaces:", result.memorySpaces.synced);
console.log(" Contexts:", result.contexts.synced);
console.log(" Memories:", result.memories.synced);
console.log(" Facts:", result.facts.synced);
console.log(" Errors:", result.errors.length);
console.log(" Duration:", result.duration, "ms");

Performance

Query Performance

Query TypeGraph-Lite (Convex)Native GraphWhen to Use
1-hop traversal3-10ms10-25msGraph-Lite
3-hop traversal10-50ms4-10msNative Graph
7-hop traversal50-200ms4-15msNative Graph
Pattern matchingNot feasible10-100msNative Graph only
Entity networksNot feasible20-50msNative Graph only

Sync Performance

  • Single entity: 30-60ms
  • Batch sync: ~300 entities/second
  • Real-time lag: <1 second with worker
  • Enrichment overhead: +90ms for 2-5x context

Error Handling

Graph Connection Errors

try {
await adapter.connect(config);
} catch (error) {
if (error.code === "CONNECTION_ERROR") {
console.error("Failed to connect to graph database");
// Fall back to Graph-Lite or disable graph features
}
}

Sync Failures

// Sync failures log warnings but don't throw
await cortex.vector.store(data, { syncToGraph: true });
// If graph sync fails: Logs warning, continues
// Convex remains source of truth

Query Errors

try {
const result = await adapter.query(cypherQuery);
} catch (error) {
if (error.name === "GraphQueryError") {
console.error("Query failed:", error.query);
}
}

Best Practices

1. Convex as Source of Truth

// ✅ Always write to Convex first
await cortex.memory.remember(params, { syncToGraph: true });
// Convex write succeeds → then sync to graph

// ❌ Don't write to graph first
await adapter.createNode({ ... }); // Graph could succeed but Convex fail

2. Use Auto-Sync for Simplicity

// ✅ Enable auto-sync, use convenience APIs
const cortex = new Cortex({
convexUrl: "...",
graph: { adapter, autoSync: true },
});

await cortex.memory.remember(params);
// Syncs automatically in background!

3. Monitor Sync Health

// Check worker periodically
const worker = cortex.getGraphSyncWorker();
if (worker) {
const metrics = worker.getMetrics();

if (metrics.queueSize > 1000) {
console.warn("Sync queue backing up!");
}

if (metrics.failureCount > 100) {
console.error("High failure rate, check graph connection");
}
}

4. Use Graph for Discovery, Not Storage

// ✅ Query graph for relationships
const related = await adapter.query(`
MATCH (m:Memory)-[:REFERENCES]->(conv:Conversation)<-[:EXTRACTED_FROM]-(f:Fact)
RETURN f
`);

// ✅ But fetch full data from Convex
for (const record of related.records) {
const fullFact = await cortex.facts.get(
memorySpaceId,
record.f.properties.factId,
);
// Full data with all versions from Convex
}

TypeScript Types

import type {
GraphAdapter,
GraphNode,
GraphEdge,
GraphPath,
GraphQuery,
GraphConnectionConfig,
TraversalConfig,
ShortestPathConfig,
SyncHealthMetrics,
GraphSyncWorkerOptions,
} from "@cortexmemory/sdk";

Examples

Complete examples in:

  • examples/graph-realtime-sync.ts - Real-time worker usage
  • tests/graph/proofs/07-multilayer-retrieval.proof.ts - Multi-layer enhancement
  • tests/graph/end-to-end-multilayer.test.ts - Complete validation

Limitations

Memgraph Compatibility

  • ✅ Basic operations: 100%
  • ✅ Traversal: 100%
  • ⚠️ shortestPath: Not supported (use traversal instead)
  • Overall: ~80% compatible

Real-Time Sync

  • Reactive (NOT polling)
  • <1s lag typical
  • Requires Convex running
  • Uses client.onUpdate() pattern

Next Steps


Questions? Ask in GitHub Discussions or Discord.