Graph Operations API
Last Updated: 2026-01-08 | Version: v0.24.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
- Multi-Tenant - Full
tenantIdsupport for SaaS isolation (NEW)
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 Integration 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(" → ")}`,
);
}
disconnect()
Close connection to graph database.
await adapter.disconnect();
// Connection closed, resources freed
isConnected()
Test if adapter is connected to the database.
const connected = await adapter.isConnected();
if (!connected) {
console.log("Reconnecting to graph database...");
await adapter.connect(config);
}
mergeNode()
Create or update a node using MERGE semantics (idempotent).
Added in v0.19.1 for resilient, idempotent operations. Safe for concurrent operations and re-running scripts.
// Idempotent - safe to call multiple times
const nodeId = await adapter.mergeNode(
{
label: "MemorySpace",
properties: {
memorySpaceId: "space-123",
name: "Main Space",
type: "personal",
createdAt: Date.now(),
},
},
{ memorySpaceId: "space-123" }, // Match properties
);
// Creates if not exists, updates if exists
getNode()
Get a node by its internal graph ID.
const node = await adapter.getNode(nodeId);
if (node) {
console.log("Label:", node.label);
console.log("Properties:", node.properties);
}
updateNode()
Update a node's properties.
await adapter.updateNode(nodeId, {
importance: 95,
updatedAt: Date.now(),
});
deleteNode()
Delete a node from the graph.
// Delete node and all connected relationships
await adapter.deleteNode(nodeId, true); // detach = true
// Delete node only (fails if has relationships)
await adapter.deleteNode(nodeId, false);
findNodes()
Find nodes by label and optional properties.
// Find all Memory nodes
const allMemories = await adapter.findNodes("Memory");
// Find Memory nodes with specific properties
const importantMemories = await adapter.findNodes(
"Memory",
{ importance: 100 },
50, // limit
);
// Find specific entity by ID property
const entity = await adapter.findNodes("Entity", { name: "Alice" }, 1);
deleteEdge()
Delete an edge (relationship) by its ID.
await adapter.deleteEdge(edgeId);
findEdges()
Find edges by type and optional properties.
// Find all REFERENCES relationships
const references = await adapter.findEdges("REFERENCES");
// Find edges with specific properties
const recentEdges = await adapter.findEdges(
"MENTIONS",
{ role: "subject" },
100, // limit
);
batchWrite()
Execute multiple operations in a single transaction.
await adapter.batchWrite([
{
type: "CREATE_NODE",
data: {
label: "Entity",
properties: { name: "Alice", type: "person" },
},
},
{
type: "CREATE_NODE",
data: {
label: "Entity",
properties: { name: "Acme Corp", type: "company" },
},
},
{
type: "CREATE_EDGE",
data: {
type: "WORKS_AT",
from: aliceNodeId,
to: acmeNodeId,
properties: { since: 2020 },
},
},
]);
// All operations succeed or fail together
Supported operation types:
CREATE_NODE- Create a new nodeUPDATE_NODE- Update node propertiesDELETE_NODE- Delete a nodeCREATE_EDGE- Create a relationshipDELETE_EDGE- Delete a relationship
countNodes()
Count nodes in the database.
// Count all nodes
const total = await adapter.countNodes();
console.log(`Total nodes: ${total}`);
// Count nodes by label
const memoryCount = await adapter.countNodes("Memory");
const factCount = await adapter.countNodes("Fact");
console.log(`Memories: ${memoryCount}, Facts: ${factCount}`);
countEdges()
Count edges (relationships) in the database.
// Count all edges
const total = await adapter.countEdges();
console.log(`Total relationships: ${total}`);
// Count edges by type
const references = await adapter.countEdges("REFERENCES");
const mentions = await adapter.countEdges("MENTIONS");
console.log(`References: ${references}, Mentions: ${mentions}`);
clearDatabase()
Clear all data from the database.
This operation deletes ALL nodes and relationships. Use only for testing or complete reset.
await adapter.clearDatabase();
Sync Operations
Automatic Graph Sync (v0.29.0+)
Important: As of v0.29.0, graph synchronization is automatic when CORTEX_GRAPH_SYNC=true is set in your environment variables.
The syncToGraph option has been removed from all APIs. Graph sync is now controlled entirely by:
- Setting
CORTEX_GRAPH_SYNC=truein your environment - Providing valid graph credentials (e.g.,
NEO4J_URI,NEO4J_USERNAME,NEO4J_PASSWORD)
Automatic Sync Behavior
// All operations auto-sync to graph when CORTEX_GRAPH_SYNC=true
// Facts are automatically synced including Entity nodes
await cortex.facts.store({
memorySpaceId: "agent-1",
fact: "Alice works at Acme Corp",
factType: "relationship",
subject: "Alice",
predicate: "works_at",
object: "Acme Corp",
entities: [
{ name: "Alice", type: "person" },
{ name: "Acme Corp", type: "organization" },
],
});
// ✅ Fact node + Entity nodes + MENTIONS relationships created automatically!
// Memories are automatically synced
await cortex.vector.store(memorySpaceId, data);
// ✅ Memory node synced automatically!
// Conversations are automatically synced
await cortex.conversations.create(input);
// ✅ Conversation node synced automatically!
Disabling Graph Sync
To disable graph sync, simply don't set CORTEX_GRAPH_SYNC or set it to false:
# Disable graph sync (default)
CORTEX_GRAPH_SYNC=false
# Or simply don't set the variable
# CORTEX_GRAPH_SYNC=...
<Note>
The syncToGraph option was removed in v0.29.0 as part of simplifying the API.
Graph sync is now a deployment-time decision controlled by environment variables.
</Note>
Manual Sync Functions
Direct sync control for power users. All sync functions support an optional tenantId parameter for multi-tenant SaaS isolation.
import {
syncMemoryToGraph,
syncFactToGraph,
syncContextToGraph,
syncConversationToGraph,
syncMemorySpaceToGraph,
syncMemoryRelationships,
syncFactRelationships,
syncContextRelationships,
syncConversationRelationships,
syncA2ARelationships,
} 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);
syncContextToGraph()
Sync a Context to the graph database using MERGE semantics (idempotent).
async function syncContextToGraph(
context: Context,
adapter: GraphAdapter,
tenantId?: string, // Optional: Multi-tenancy isolation
): Promise<string>; // Returns graph node ID
Example:
const context = await cortex.contexts.get(contextId);
const nodeId = await syncContextToGraph(context, adapter, "tenant-acme");
await syncContextRelationships(context, nodeId, adapter);
syncConversationToGraph()
Sync a Conversation to the graph database using MERGE semantics.
async function syncConversationToGraph(
conversation: Conversation,
adapter: GraphAdapter,
tenantId?: string, // Optional: Multi-tenancy isolation
): Promise<string>; // Returns graph node ID
Example:
const conversation = await cortex.conversations.get(conversationId);
const nodeId = await syncConversationToGraph(conversation, adapter);
await syncConversationRelationships(conversation, nodeId, adapter);
syncMemoryToGraph()
Sync a Memory to the graph database. Content is truncated to 200 characters in the graph node.
async function syncMemoryToGraph(
memory: MemoryEntry,
adapter: GraphAdapter,
tenantId?: string, // Optional: Multi-tenancy isolation
): Promise<string>; // Returns graph node ID
Example:
const memory = await cortex.vector.get(memorySpaceId, memoryId);
const nodeId = await syncMemoryToGraph(memory, adapter, "tenant-acme");
await syncMemoryRelationships(memory, nodeId, adapter);
// For A2A memories, also sync A2A relationships
if (memory.sourceType === "a2a") {
await syncA2ARelationships(memory, adapter);
}
syncFactToGraph()
Sync a Fact to the graph database using MERGE semantics.
async function syncFactToGraph(
fact: FactRecord,
adapter: GraphAdapter,
tenantId?: string, // Optional: Multi-tenancy isolation
): Promise<string>; // Returns graph node ID
Example:
const fact = await cortex.facts.get(memorySpaceId, factId);
const nodeId = await syncFactToGraph(fact, adapter);
await syncFactRelationships(fact, nodeId, adapter);
syncMemorySpaceToGraph()
Sync a MemorySpace to the graph database using MERGE semantics.
async function syncMemorySpaceToGraph(
memorySpace: MemorySpace,
adapter: GraphAdapter,
tenantId?: string, // Optional: Multi-tenancy isolation
): Promise<string>; // Returns graph node ID
Example:
const memorySpace = await cortex.memorySpaces.get(memorySpaceId);
const nodeId = await syncMemorySpaceToGraph(memorySpace, adapter);
// MemorySpace nodes don't have additional relationships to sync
syncConversationRelationships()
Sync Conversation relationships to the graph. Creates IN_SPACE, INVOLVES, and HAS_PARTICIPANT edges.
async function syncConversationRelationships(
conversation: Conversation,
conversationNodeId: string,
adapter: GraphAdapter,
): Promise<void>;
syncA2ARelationships()
Sync Agent-to-Agent (A2A) communication relationships. Creates SENT_TO edges between memory spaces.
async function syncA2ARelationships(
memory: MemoryEntry,
adapter: GraphAdapter,
): Promise<void>;
Example:
// Only call for A2A memories
if (memory.sourceType === "a2a") {
await syncA2ARelationships(memory, adapter);
}
Helper Functions
Utility functions for managing graph nodes and lookups.
findGraphNodeId()
Find a graph node ID by Cortex entity ID.
import { findGraphNodeId } from "@cortexmemory/sdk/graph";
async function findGraphNodeId(
label: string, // 'Context', 'Conversation', 'Memory', 'Fact', 'MemorySpace', 'User', 'Agent', 'Participant'
cortexId: string, // The Cortex entity ID (e.g., memoryId, factId)
adapter: GraphAdapter,
): Promise<string | null>; // Returns graph node ID or null if not found
Example:
// Find a Memory node by memoryId
const nodeId = await findGraphNodeId("Memory", "mem-123", adapter);
if (nodeId) {
const node = await adapter.getNode(nodeId);
console.log("Found memory:", node?.properties);
}
// Find a Fact node by factId
const factNodeId = await findGraphNodeId("Fact", "fact-456", adapter);
ensureUserNode()
Create or get a User node using MERGE semantics (idempotent).
import { ensureUserNode } from "@cortexmemory/sdk/graph";
async function ensureUserNode(
userId: string,
adapter: GraphAdapter,
tenantId?: string, // Optional: Multi-tenancy isolation
): Promise<string>; // Returns graph node ID
Example:
// Ensure user exists in graph before creating relationships
const userNodeId = await ensureUserNode("user-123", adapter, "tenant-acme");
ensureAgentNode()
Create or get an Agent node using MERGE semantics (idempotent).
import { ensureAgentNode } from "@cortexmemory/sdk/graph";
async function ensureAgentNode(
agentId: string,
adapter: GraphAdapter,
tenantId?: string, // Optional: Multi-tenancy isolation
): Promise<string>; // Returns graph node ID
Example:
// Ensure agent exists in graph
const agentNodeId = await ensureAgentNode("assistant-v1", adapter);
ensureParticipantNode()
Create or get a Participant node for Hive Mode using MERGE semantics.
import { ensureParticipantNode } from "@cortexmemory/sdk/graph";
async function ensureParticipantNode(
participantId: string,
adapter: GraphAdapter,
tenantId?: string, // Optional: Multi-tenancy isolation
): Promise<string>; // Returns graph node ID
Example:
// Ensure participant exists for Hive Mode tracking
const participantNodeId = await ensureParticipantNode(
"participant-alice",
adapter,
);
ensureEntityNode()
Create or get an Entity node using MERGE semantics. Used for fact subject/object entities.
import { ensureEntityNode } from "@cortexmemory/sdk/graph";
async function ensureEntityNode(
entityName: string, // Entity name (e.g., "Alice", "Acme Corp")
entityType: string, // Entity type (e.g., "subject", "object", "person", "company")
adapter: GraphAdapter,
tenantId?: string, // Optional: Multi-tenancy isolation
): Promise<string>; // Returns graph node ID
Example:
// Create entity nodes for a fact relationship
const aliceNodeId = await ensureEntityNode("Alice", "person", adapter);
const acmeNodeId = await ensureEntityNode("Acme Corp", "company", adapter);
// Create relationship between entities
await adapter.createEdge({
type: "WORKS_AT",
from: aliceNodeId,
to: acmeNodeId,
properties: { since: 2020 },
});
ensureEnrichedEntityNode()
Create or get an enriched Entity node with additional metadata from bullet-proof extraction.
import { ensureEnrichedEntityNode } from "@cortexmemory/sdk/graph";
async function ensureEnrichedEntityNode(
entityName: string, // Entity name (e.g., "Alex")
entityType: string, // Specific type (e.g., "preferred_name", "full_name")
fullValue: string | undefined, // Full value if available (e.g., "Alexander Johnson")
adapter: GraphAdapter,
tenantId?: string, // Optional: Multi-tenancy isolation
): Promise<string>; // Returns graph node ID
Example:
// Create enriched entity with full value context
const entityNodeId = await ensureEnrichedEntityNode(
"Alex", // Short name
"preferred_name", // Specific entity type
"Alexander Johnson", // Full value
adapter,
);
// Node properties: { name: "Alex", type: "preferred_name", entityType: "preferred_name", fullValue: "Alexander Johnson" }
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
// Graph cleanup is automatic when CORTEX_GRAPH_SYNC=true (v0.29.0+)
await cortex.memory.forget("agent-1", "mem-123", {
deleteConversation: true,
});
// 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
Delete Functions
Direct delete functions for programmatic graph cleanup.
import {
deleteMemoryFromGraph,
deleteFactFromGraph,
deleteContextFromGraph,
deleteConversationFromGraph,
deleteMemorySpaceFromGraph,
} from "@cortexmemory/sdk/graph";
deleteMemoryFromGraph()
Delete a memory node with optional orphan cleanup.
const result = await deleteMemoryFromGraph(
"mem-123", // memoryId
adapter,
true, // enableOrphanCleanup (default: true)
);
console.log("Deleted nodes:", result.deletedNodes.length);
console.log("Orphan islands:", result.orphanIslands.length);
deleteFactFromGraph()
Delete a fact node and cascade to orphaned entities.
const result = await deleteFactFromGraph(
"fact-456", // factId
adapter,
true, // enableOrphanCleanup
);
// Entities mentioned only by this fact are also deleted
deleteContextFromGraph()
Delete a context node with relationship cleanup.
const result = await deleteContextFromGraph(
"ctx-789", // contextId
adapter,
true, // enableOrphanCleanup
);
deleteConversationFromGraph()
Delete a conversation node.
const result = await deleteConversationFromGraph(
"conv-abc", // conversationId
adapter,
true, // enableOrphanCleanup
);
deleteMemorySpaceFromGraph()
Delete a memory space node (use with caution).
Does NOT cascade to memories/contexts in that space. Use with caution.
const result = await deleteMemorySpaceFromGraph(
"space-123", // memorySpaceId
adapter,
);
deleteImmutableFromGraph()
Delete an immutable record from the graph. For facts, uses deleteFactFromGraph internally.
import { deleteImmutableFromGraph } from "@cortexmemory/sdk/graph";
async function deleteImmutableFromGraph(
immutableType: string, // Type of immutable record (e.g., "fact")
immutableId: string, // Record ID
adapter: GraphAdapter,
enableOrphanCleanup?: boolean, // Default: true
): Promise<DeleteResult>;
Example:
// Delete a fact via immutable API
const result = await deleteImmutableFromGraph(
"fact",
"fact-123",
adapter,
true,
);
// Delete other immutable types
const result2 = await deleteImmutableFromGraph("document", "doc-456", adapter);
deleteMutableFromGraph()
Delete a mutable record from the graph. Simple delete with no cascading.
import { deleteMutableFromGraph } from "@cortexmemory/sdk/graph";
async function deleteMutableFromGraph(
namespace: string, // Mutable namespace
key: string, // Mutable key
adapter: GraphAdapter,
): Promise<DeleteResult>;
Example:
const result = await deleteMutableFromGraph(
"user_preferences",
"theme",
adapter,
);
Orphan Detection
Handles complex scenarios:
// Scenario: Circular references
// Entity A → KNOWS → Entity B
// Entity B → KNOWS → Entity A
// Fact F1 → MENTIONS → 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
Programmatic Orphan Detection
For advanced use cases, access the orphan detection system directly.
import {
ORPHAN_RULES,
createDeletionContext,
deleteWithOrphanCleanup,
detectOrphan,
canRunOrphanCleanup,
} from "@cortexmemory/sdk/graph";
ORPHAN_RULES
Default orphan rules for each node type.
const rules = ORPHAN_RULES;
/*
{
Conversation: { keepIfReferencedBy: ['Memory', 'Fact', 'Context'] },
Entity: { keepIfReferencedBy: ['Fact'] },
User: { neverDelete: true },
Participant: { neverDelete: true },
MemorySpace: { neverDelete: true },
Memory: { explicitOnly: true },
Fact: { explicitOnly: true },
Context: { explicitOnly: true },
}
*/
createDeletionContext()
Create a context for tracking cascading deletes.
const deletionContext = createDeletionContext(
"Delete Memory mem-123", // reason (for logging)
ORPHAN_RULES, // optional custom rules
);
// Use with deleteWithOrphanCleanup
const result = await deleteWithOrphanCleanup(
nodeId,
"Memory",
deletionContext,
adapter,
);
deleteWithOrphanCleanup()
Low-level delete with orphan cascade.
const result = await deleteWithOrphanCleanup(
nodeId, // Graph node ID
"Memory", // Node label
deletionContext,
adapter,
);
// Result type: DeleteResult
console.log("Deleted nodes:", result.deletedNodes);
console.log("Deleted edges:", result.deletedEdges);
console.log("Orphan islands:", result.orphanIslands);
detectOrphan()
Check if a node would be orphaned.
const check = await detectOrphan(
nodeId,
"Conversation",
deletionContext,
adapter,
);
console.log("Is orphan:", check.isOrphan);
console.log("Reason:", check.reason);
console.log("Referenced by:", check.referencedBy);
console.log("Part of circular island:", check.partOfCircularIsland);
canRunOrphanCleanup()
Check if cleanup is safe to run.
const canRun = await canRunOrphanCleanup(adapter);
if (canRun) {
// Safe to proceed with cleanup
}
Belief Revision (Fact Supersession)
Added in v0.24.0
Functions for managing fact versioning and supersession in the graph database. These functions sync belief revision actions to maintain fact lineage and provenance.
syncFactSupersession()
Sync a fact supersession to the graph database. Creates a SUPERSEDES relationship from the new fact to the old fact.
import { syncFactSupersession } from "@cortexmemory/sdk/graph";
async function syncFactSupersession(
oldFact: FactRecord, // The fact being superseded
newFact: FactRecord, // The fact that supersedes
adapter: GraphAdapter,
reason?: string, // Optional reason for supersession
tenantId?: string, // Optional: Multi-tenancy isolation
): Promise<{
oldNodeId: string; // Graph ID of superseded fact
newNodeId: string; // Graph ID of new fact
relationshipId: string; // Graph ID of SUPERSEDES relationship
}>;
Example:
// When belief revision determines a fact should be superseded
const result = await syncFactSupersession(
oldFact,
newFact,
adapter,
"User corrected their preference",
"tenant-acme",
);
console.log(`Fact ${newFact.factId} supersedes ${oldFact.factId}`);
// Creates: (newFact)-[:SUPERSEDES]->(oldFact)
updateFactGraphStatus()
Update the status of a fact node in the graph.
import { updateFactGraphStatus } from "@cortexmemory/sdk/graph";
async function updateFactGraphStatus(
factId: string,
status: "active" | "superseded" | "deleted",
adapter: GraphAdapter,
): Promise<void>;
Example:
// Mark a fact as superseded
await updateFactGraphStatus("fact-123", "superseded", adapter);
// Mark a fact as deleted (soft delete)
await updateFactGraphStatus("fact-456", "deleted", adapter);
syncFactUpdateInPlace()
Sync an in-place fact update to the graph. Used for UPDATE action in belief revision (content refinement without supersession).
import { syncFactUpdateInPlace } from "@cortexmemory/sdk/graph";
async function syncFactUpdateInPlace(
fact: FactRecord, // The updated fact
adapter: GraphAdapter,
tenantId?: string, // Optional: Multi-tenancy isolation
): Promise<string>; // Returns graph node ID
Example:
// When belief revision merges/updates an existing fact
const nodeId = await syncFactUpdateInPlace(updatedFact, adapter);
// Fact node is updated with new content, status remains "active"
syncFactRevision()
Create a REVISED_FROM relationship between facts. Used when a fact is refined (content updated) rather than superseded.
import { syncFactRevision } from "@cortexmemory/sdk/graph";
async function syncFactRevision(
originalFact: FactRecord, // The original fact
revisedFact: FactRecord, // The revised fact
adapter: GraphAdapter,
reason?: string, // Optional reason for revision
tenantId?: string, // Optional: Multi-tenancy isolation
): Promise<{
originalNodeId: string;
revisedNodeId: string;
relationshipId: string;
}>;
Example:
// Create revision relationship for fact refinement
const result = await syncFactRevision(
originalFact,
revisedFact,
adapter,
"Added more specific details",
);
// Creates: (revisedFact)-[:REVISED_FROM]->(originalFact)
getFactSupersessionChainFromGraph()
Get the complete supersession chain for a fact from the graph.
import { getFactSupersessionChainFromGraph } from "@cortexmemory/sdk/graph";
async function getFactSupersessionChainFromGraph(
factId: string,
adapter: GraphAdapter,
): Promise<
Array<{
factId: string;
fact: string;
supersededAt?: number;
}>
>;
Example:
// Get the full history of a fact
const chain = await getFactSupersessionChainFromGraph("fact-123", adapter);
for (const item of chain) {
console.log(`${item.factId}: ${item.fact}`);
if (item.supersededAt) {
console.log(` Superseded at: ${new Date(item.supersededAt)}`);
}
}
// Returns facts ordered from oldest to newest
removeFactSupersessionRelationships()
Remove supersession relationships for a fact. Used when undoing a supersession or during cleanup.
import { removeFactSupersessionRelationships } from "@cortexmemory/sdk/graph";
async function removeFactSupersessionRelationships(
factId: string,
adapter: GraphAdapter,
): Promise<number>; // Returns count of deleted relationships
Example:
// Remove all SUPERSEDES relationships for a fact
const deleted = await removeFactSupersessionRelationships("fact-123", adapter);
console.log(`Removed ${deleted} supersession relationships`);
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);
console.log("Missing constraints:", status.missing);
Return type:
interface VerifySchemaResult {
/** Whether all required constraints exist */
valid: boolean;
/** List of existing constraint names */
constraints: string[];
/** List of existing index names */
indexes: string[];
/** List of missing required constraint names */
missing: string[];
}
dropGraphSchema()
Remove all constraints and indexes (testing/reset).
Removes all schema constraints and indexes. Use only for testing or reset scenarios.
import { dropGraphSchema } from "@cortexmemory/sdk/graph";
await dropGraphSchema(adapter);
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!
Multi-Tenancy Support
All graph nodes include tenantId for SaaS multi-tenant isolation:
// When syncing with tenant context
await syncMemoryToGraph(memory, adapter, "tenant-acme");
// Cypher queries respect tenant isolation
const query = `
MATCH (m:Memory)
WHERE m.tenantId = $tenantId AND m.userId = $userId
RETURN m
`;
// GDPR cascade includes tenant context
// User deletion traverses graph nodes with matching tenantId
All Node Types Include:
(:AnyNode {
tenantId: string | null, // SaaS isolation
// ... other properties
})
Node Types
MemorySpace
(:MemorySpace {
memorySpaceId: string,
tenantId: string | null, // Multi-tenancy
name: string,
type: string, // 'personal', 'team', 'project', 'custom'
status: string,
createdAt: number
})
Context
(:Context {
contextId: string,
memorySpaceId: string,
tenantId: string | null, // Multi-tenancy
purpose: string,
status: string,
depth: number,
parentId: string,
rootId: string,
createdAt: number
})
Memory
(:Memory {
memoryId: string,
memorySpaceId: string,
tenantId: string | null, // Multi-tenancy
content: string, // Truncated (first 200 chars)
importance: number,
sourceType: string,
tags: array,
createdAt: number
})
Fact
(:Fact {
factId: string,
memorySpaceId: string,
tenantId: string | null, // Multi-tenancy
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)
Automatic Graph Sync Reference (v0.29.0+)
<Note>
Breaking Change in v0.29.0: The syncToGraph option has been removed from all APIs.
Graph synchronization is now automatic when CORTEX_GRAPH_SYNC=true is set.
</Note>
All Cortex APIs automatically sync to the graph database when:
CORTEX_GRAPH_SYNC=trueis set in environment variables- Valid graph credentials are configured
Conversations API
// Automatically syncs when CORTEX_GRAPH_SYNC=true
await cortex.conversations.create(input);
await cortex.conversations.addMessage(input);
await cortex.conversations.delete(id);
Vector API
// Automatically syncs when CORTEX_GRAPH_SYNC=true
await cortex.vector.store(memorySpaceId, input);
await cortex.vector.update(memorySpaceId, memoryId, updates);
await cortex.vector.delete(memorySpaceId, memoryId);
Facts API
// Automatically syncs when CORTEX_GRAPH_SYNC=true
// Includes Entity nodes and MENTIONS relationships
await cortex.facts.store(params);
await cortex.facts.update(memorySpaceId, factId, updates);
await cortex.facts.delete(memorySpaceId, factId);
Contexts API
// Automatically syncs when CORTEX_GRAPH_SYNC=true
await cortex.contexts.create(params);
await cortex.contexts.update(contextId, updates);
await cortex.contexts.delete(contextId);
Memory Spaces API
// Automatically syncs when CORTEX_GRAPH_SYNC=true
await cortex.memorySpaces.register(params);
Memory API (Convenience)
// Automatically syncs when CORTEX_GRAPH_SYNC=true
await cortex.memory.remember(params);
// Forget with cascade (graph orphan cleanup is automatic)
await cortex.memory.forget(memorySpaceId, memoryId, {
deleteConversation: true,
});
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 Type | Graph-Lite (Convex) | Native Graph | When to Use |
|---|---|---|---|
| 1-hop traversal | 3-10ms | 10-25ms | Graph-Lite |
| 3-hop traversal | 10-50ms | 4-10ms | Native Graph |
| 7-hop traversal | 50-200ms | 4-15ms | Native Graph |
| Pattern matching | Not feasible | 10-100ms | Native Graph only |
| Entity networks | Not feasible | 20-50ms | Native 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 instanceof GraphAuthenticationError) {
console.error(`Auth failed for ${error.username} at ${error.uri}`);
console.error("Check NEO4J_USERNAME and NEO4J_PASSWORD");
} else if (error instanceof GraphConnectionError) {
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 (v0.29.0+)
// Graph sync is automatic when CORTEX_GRAPH_SYNC=true
await cortex.vector.store(data);
// 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
// Recommended: Always write to Convex first (v0.29.0+)
// Graph sync is automatic when CORTEX_GRAPH_SYNC=true
await cortex.memory.remember(params);
// Convex write succeeds → then auto-syncs to graph
// Avoid: Writing to graph first
await adapter.createNode({ ... }); // Graph could succeed but Convex fail
2. Use Auto-Sync for Simplicity
// Recommended: 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
// Good: Query graph for relationships
const related = await adapter.query(`
MATCH (m:Memory)-[:REFERENCES]->(conv:Conversation)<-[:EXTRACTED_FROM]-(f:Fact)
RETURN f
`);
// Good: 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
Core Types
import type {
// Adapter Interface
GraphAdapter,
// Node & Edge Types
GraphNode,
GraphEdge,
GraphPath,
// Query Types
GraphQuery,
GraphQueryResult,
QueryStatistics,
// Configuration
GraphConnectionConfig,
GraphOperation,
TraversalConfig,
ShortestPathConfig,
// Sync Worker
SyncHealthMetrics,
GraphSyncWorkerOptions,
// Batch Sync
BatchSyncOptions,
BatchSyncResult,
// Orphan Detection
OrphanRule,
DeletionContext,
DeleteResult,
OrphanCheckResult,
} from "@cortexmemory/sdk/graph";
Sync Functions
import {
// Node Sync
syncContextToGraph,
syncConversationToGraph,
syncMemoryToGraph,
syncFactToGraph,
syncMemorySpaceToGraph,
// Relationship Sync
syncContextRelationships,
syncConversationRelationships,
syncMemoryRelationships,
syncFactRelationships,
syncA2ARelationships,
// Helper Functions
findGraphNodeId,
ensureUserNode,
ensureAgentNode,
ensureParticipantNode,
ensureEntityNode,
ensureEnrichedEntityNode,
// Belief Revision
syncFactSupersession,
updateFactGraphStatus,
syncFactUpdateInPlace,
syncFactRevision,
getFactSupersessionChainFromGraph,
removeFactSupersessionRelationships,
// Delete Functions
deleteMemoryFromGraph,
deleteFactFromGraph,
deleteContextFromGraph,
deleteConversationFromGraph,
deleteMemorySpaceFromGraph,
deleteImmutableFromGraph,
deleteMutableFromGraph,
// Orphan Detection
ORPHAN_RULES,
createDeletionContext,
deleteWithOrphanCleanup,
detectOrphan,
canRunOrphanCleanup,
// Batch Sync
initialGraphSync,
} from "@cortexmemory/sdk/graph";
GraphNode
Represents a node in the graph database.
interface GraphNode {
/** Node label (e.g., 'Context', 'Memory', 'Fact') */
label: string;
/** Node properties (key-value pairs) */
properties: Record<string, unknown>;
/** Optional node ID (set by graph database after creation) */
id?: string;
}
GraphEdge
Represents a relationship between nodes.
interface GraphEdge {
/** Relationship type (e.g., 'CHILD_OF', 'MENTIONS', 'SENT_TO') */
type: string;
/** Source node ID */
from: string;
/** Target node ID */
to: string;
/** Optional relationship properties */
properties?: Record<string, unknown>;
/** Optional edge ID (set by graph database after creation) */
id?: string;
}
GraphPath
Path between nodes in the graph.
interface GraphPath {
/** Nodes in the path */
nodes: GraphNode[];
/** Relationships in the path */
relationships: GraphEdge[];
/** Path length (number of hops) */
length: number;
}
GraphQuery
Cypher query with parameters.
interface GraphQuery {
/** Cypher query string */
cypher: string;
/** Query parameters (for parameterized queries) */
params?: Record<string, unknown>;
}
GraphQueryResult
Result from a graph query.
interface GraphQueryResult {
/** Records returned by the query */
records: Record<string, unknown>[];
/** Number of records returned */
count: number;
/** Optional query statistics */
stats?: QueryStatistics;
}
QueryStatistics
Query execution statistics.
interface QueryStatistics {
nodesCreated?: number;
nodesDeleted?: number;
relationshipsCreated?: number;
relationshipsDeleted?: number;
propertiesSet?: number;
labelsAdded?: number;
labelsRemoved?: number;
indexesAdded?: number;
constraintsAdded?: number;
}
GraphConnectionConfig
Connection configuration for graph database.
interface GraphConnectionConfig {
/** Bolt URI (e.g., 'bolt://localhost:7687') */
uri: string;
/** Username for authentication */
username: string;
/** Password for authentication */
password: string;
/** Optional database name (Neo4j 4.0+) */
database?: string;
/** Optional connection pool configuration */
maxConnectionPoolSize?: number;
/** Optional connection timeout (ms) */
connectionTimeout?: number;
}
GraphOperation
Batch operation for graph write.
interface GraphOperation {
/** Operation type */
type:
| "CREATE_NODE"
| "UPDATE_NODE"
| "DELETE_NODE"
| "CREATE_EDGE"
| "DELETE_EDGE";
/** Operation data */
data:
| GraphNode
| GraphEdge
| { id: string; properties?: Record<string, unknown> };
}
TraversalConfig
Configuration for graph traversal.
interface TraversalConfig {
/** Starting node ID */
startId: string;
/** Relationship types to follow (empty = all types) */
relationshipTypes?: string[];
/** Maximum depth to traverse */
maxDepth: number;
/** Optional direction: 'OUTGOING', 'INCOMING', or 'BOTH' */
direction?: "OUTGOING" | "INCOMING" | "BOTH";
/** Optional filter predicate (Cypher WHERE clause) */
filter?: string;
/** Optional filter parameters */
filterParams?: Record<string, unknown>;
}
ShortestPathConfig
Configuration for shortest path queries.
interface ShortestPathConfig {
/** Source node ID */
fromId: string;
/** Target node ID */
toId: string;
/** Maximum number of hops to search */
maxHops: number;
/** Optional relationship types to follow */
relationshipTypes?: string[];
/** Optional direction */
direction?: "OUTGOING" | "INCOMING" | "BOTH";
}
Orphan Detection Types
import type {
OrphanRule,
DeletionContext,
DeleteResult,
OrphanCheckResult,
} from "@cortexmemory/sdk/graph";
Error Classes
import {
GraphDatabaseError,
GraphConnectionError,
GraphAuthenticationError,
GraphQueryError,
GraphNotFoundError,
} from "@cortexmemory/sdk/graph";
OrphanRule
Orphan detection rules for node types.
interface OrphanRule {
/** Node types that must reference this node to keep it alive */
keepIfReferencedBy?: string[];
/** Never auto-delete this node type */
neverDelete?: boolean;
/** Only delete if explicitly requested (not cascaded) */
explicitOnly?: boolean;
}
DeletionContext
Context for tracking cascading deletes.
interface DeletionContext {
/** Set of node IDs being deleted in this operation */
deletedNodeIds: Set<string>;
/** Reason for deletion (for logging/debugging) */
reason: string;
/** Timestamp of deletion */
timestamp: number;
/** Orphan rules to use (can be customized) */
orphanRules?: Record<string, OrphanRule>;
}
DeleteResult
Result of a delete operation with orphan cleanup.
interface DeleteResult {
/** IDs of all nodes deleted (including cascaded orphans) */
deletedNodes: string[];
/** IDs of all edges deleted */
deletedEdges: string[];
/** Orphan islands that were removed */
orphanIslands: Array<{
nodes: string[];
reason: string;
}>;
}
OrphanCheckResult
Result of orphan detection check.
interface OrphanCheckResult {
/** Is this node an orphan? */
isOrphan: boolean;
/** Reason for orphan status */
reason: string;
/** IDs of nodes that reference this node */
referencedBy: string[];
/** Is this part of a circular orphan island? */
partOfCircularIsland: boolean;
/** If part of island, the full island node IDs */
islandNodes?: string[];
}
Error Classes
import {
GraphDatabaseError,
GraphConnectionError,
GraphAuthenticationError,
GraphQueryError,
GraphNotFoundError,
} from "@cortexmemory/sdk/graph";
- GraphDatabaseError - Base error for graph operations
- GraphConnectionError - Connection failures (code:
CONNECTION_ERROR) - GraphAuthenticationError - Authentication failures (subclass of
GraphConnectionError) - GraphQueryError - Query execution failures (includes query string)
- GraphNotFoundError - Node or edge not found (code:
NOT_FOUND)
GraphAuthenticationError
Specific error class for authentication failures. Provides additional context for debugging credential issues.
class GraphAuthenticationError extends GraphConnectionError {
readonly name: "GraphAuthenticationError";
readonly uri: string; // The connection URI that failed
readonly username: string; // The username that was used
constructor(message: string, uri: string, username: string, cause?: Error);
}
Example:
try {
await adapter.connect({
uri: "bolt://localhost:7687",
username: "neo4j",
password: "wrong-password",
});
} catch (error) {
if (error instanceof GraphAuthenticationError) {
console.error(
`Authentication failed for ${error.username} at ${error.uri}`,
);
console.error(
"Check NEO4J_USERNAME and NEO4J_PASSWORD environment variables",
);
} else if (error instanceof GraphConnectionError) {
console.error("Connection failed - is the database running?");
}
}
Examples
Complete examples in:
examples/graph-realtime-sync.ts- Real-time worker usagetests/graph/proofs/07-multilayer-retrieval.proof.ts- Multi-layer enhancementtests/graph/end-to-end-multilayer.test.ts- Complete validation
Limitations
Memgraph Compatibility
- Basic operations: 100% compatible
- Traversal: 100% compatible
- 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
- Graph Database Integration - Complete integration guide (setup, selection, and patterns)
Questions? Ask in GitHub Discussions.