Skip to main content

Graph Operations API

Info

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 tenantId support 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

Terminal
$ 
# 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 node
  • UPDATE_NODE - Update node properties
  • DELETE_NODE - Delete a node
  • CREATE_EDGE - Create a relationship
  • DELETE_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.

Danger

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:

  1. Setting CORTEX_GRAPH_SYNC=true in your environment
  2. 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).

Warning

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

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)

Info

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).

Danger

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:

  1. CORTEX_GRAPH_SYNC=true is set in environment variables
  2. 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 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 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 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% 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


Questions? Ask in GitHub Discussions.