Skip to main content

Mutable Store API

Last Updated: 2025-10-28

Complete API reference for shared mutable data with ACID transaction guarantees.

Overview

The Mutable Store API (Layer 1c) provides methods for storing TRULY SHARED mutable data across ALL memory spaces. This layer has NO memorySpace scoping - it's globally shared just like Layer 1b (Immutable).

Critical Distinction:

  • NO memorySpaceId parameter - This layer is shared across ALL spaces
  • ✅ Accessible from any memory space
  • ✅ Perfect for: Inventory, config, counters, live shared state

Key Characteristics:

  • TRULY Shared - ALL memory spaces can access
  • NO Isolation - Not scoped to memorySpace (unlike L1a, L2, L3)
  • Mutable - Designed to be updated in-place
  • ACID - Atomic transactions
  • Current-value - No version history (by design)
  • Fast - Optimized for frequent updates
  • Purgeable - Can delete keys

Comparison to Other Stores:

FeatureConversations (1a)Immutable (1b)Mutable (1c)Vector (2)Facts (3)
ScopingmemorySpaceNO scopingNO scopingmemorySpacememorySpace
PrivacyPrivate to spaceTRULY SharedTRULY SharedPrivate to spacePrivate to space
VersioningN/A (append)AutoNoneAutoAuto
UpdatesAppend onlyNew versionIn-placeNew versionNew version
Use CaseChatsKB, policiesLive dataSearch indexExtracted facts

Core Operations

set()

Set a key to a value (creates or overwrites).

Signature:

cortex.mutable.set(
namespace: string,
key: string,
value: any,
userId?: string
): Promise<MutableRecord>

Parameters:

  • namespace (string) - Logical grouping (e.g., 'inventory', 'config', 'counters')
  • key (string) - Unique key within namespace
  • value (any) - JSON-serializable value
  • userId (string, optional) - Link to user (enables GDPR cascade). Must reference existing user.

Returns:

interface MutableRecord {
namespace: string;
key: string;
value: any;
userId?: string; // OPTIONAL: User link (GDPR-enabled)
updatedAt: Date;
createdAt: Date;
accessCount: number;
lastAccessed?: Date;
}

Example 1: System data (no userId)

// Set inventory quantity (system-wide)
const record = await cortex.mutable.set("inventory", "widget-qty", 100);

console.log(record.value); // 100
console.log(record.userId); // undefined (system data)

// Update (overwrites)
await cortex.mutable.set("inventory", "widget-qty", 95); // Now 95

// No version history - previous value (100) is gone!

Example 2: User-specific data (with userId)

// Set user session data (GDPR-enabled)
const session = await cortex.mutable.set(
"user-sessions",
"session-abc123",
{
startedAt: new Date(),
lastActivity: new Date(),
pagesViewed: 5,
},
"user-123", // ← Links to user (enables GDPR cascade)
);

console.log(session.userId); // "user-123"

// When user requests deletion:
await cortex.users.delete("user-123", { cascade: true });
// This session is automatically deleted! ✅

Errors:

  • CortexError('INVALID_NAMESPACE') - Namespace is empty or invalid
  • CortexError('INVALID_KEY') - Key is empty or invalid
  • CortexError('VALUE_TOO_LARGE') - Value exceeds size limit
  • CortexError('USER_NOT_FOUND') - userId doesn't reference existing user
  • CortexError('CONVEX_ERROR') - Database error

get()

Get current value for a key.

Signature:

cortex.mutable.get(
namespace: string,
key: string
): Promise<any | null>

Parameters:

  • namespace (string) - Namespace
  • key (string) - Key

Returns:

  • Value - Current value
  • null - If key doesn't exist

Side Effects:

  • Increments accessCount
  • Updates lastAccessed

Example:

const qty = await cortex.mutable.get("inventory", "widget-qty");

if (qty !== null) {
console.log(`Current quantity: ${qty}`);

if (qty > 0) {
// Process order
}
} else {
console.log("Product not found");
}

update()

Atomically update a value.

Signature:

cortex.mutable.update(
namespace: string,
key: string,
updater: (current: any) => any
): Promise<MutableRecord>

Parameters:

  • namespace (string) - Namespace
  • key (string) - Key
  • updater (function) - Function that receives current value, returns new value

Returns:

  • MutableRecord - Updated record

Side Effects:

  • ACID: Read, compute, write atomically
  • No race conditions (Convex handles locking)

Example:

// Decrement inventory
await cortex.mutable.update("inventory", "widget-qty", (current) => {
if (current === null || current <= 0) {
throw new Error("Out of stock");
}
return current - 1;
});

// Increment counter
await cortex.mutable.update("counters", "total-refunds", (current) => {
return (current || 0) + 1;
});

// Update object
await cortex.mutable.update("config", "api-settings", (current) => {
return {
...current,
endpoint: "https://api.example.com/v2",
timeout: 30000,
};
});

Errors:

  • CortexError('KEY_NOT_FOUND') - Key doesn't exist
  • CortexError('UPDATE_FAILED') - Updater function threw error
  • CortexError('CONVEX_ERROR') - Database error

increment()

Atomically increment a numeric value (convenience helper).

Signature:

cortex.mutable.increment(
namespace: string,
key: string,
amount?: number
): Promise<MutableRecord>

Parameters:

  • namespace (string) - Namespace
  • key (string) - Key
  • amount (number, optional) - Amount to increment (default: 1)

Returns:

  • MutableRecord - Updated record with incremented value

Example:

// Initialize counter
await cortex.mutable.set("counters", "page-views", 0);

// Increment by 1 (default)
await cortex.mutable.increment("counters", "page-views");

// Increment by custom amount
await cortex.mutable.increment("counters", "page-views", 10);

const views = await cortex.mutable.get("counters", "page-views");
console.log(`Total views: ${views}`); // 11

Note: This is a convenience wrapper around update(). For more control, use update() directly.


decrement()

Atomically decrement a numeric value (convenience helper).

Signature:

cortex.mutable.decrement(
namespace: string,
key: string,
amount?: number
): Promise<MutableRecord>

Parameters:

  • namespace (string) - Namespace
  • key (string) - Key
  • amount (number, optional) - Amount to decrement (default: 1)

Returns:

  • MutableRecord - Updated record with decremented value

Example:

// Initialize inventory
await cortex.mutable.set("inventory", "widget-qty", 100);

// Customer order (decrement)
await cortex.mutable.decrement("inventory", "widget-qty", 5);

const remaining = await cortex.mutable.get("inventory", "widget-qty");
console.log(`Remaining: ${remaining}`); // 95

getRecord()

Get full record with metadata (not just the value).

Signature:

cortex.mutable.getRecord(
namespace: string,
key: string
): Promise<MutableRecord | null>

Returns:

interface MutableRecord {
namespace: string;
key: string;
value: any;
userId?: string;
metadata?: any;
createdAt: Date;
updatedAt: Date;
accessCount: number;
lastAccessed?: Date;
}

Example:

// get() returns value only
const value = await cortex.mutable.get("config", "timeout");
console.log(value); // 30

// getRecord() returns full record with metadata
const record = await cortex.mutable.getRecord("config", "timeout");
console.log(record.value); // 30
console.log(record.createdAt); // 2025-10-26T...
console.log(record.updatedAt); // 2025-10-26T...
console.log(record.accessCount); // 15

Use when: You need access to timestamps, accessCount, or metadata.


delete()

Delete a key.

Signature:

cortex.mutable.delete(
namespace: string,
key: string
): Promise<DeleteResult>

Returns:

interface DeleteResult {
deleted: boolean;
namespace: string;
key: string;
deletedAt: Date;
}

Example:

// Remove product from inventory
const result = await cortex.mutable.delete("inventory", "discontinued-widget");

console.log(`Deleted: ${result.deleted}`);

transaction()

Execute multiple operations atomically.

Signature:

cortex.mutable.transaction(
callback: (tx: Transaction) => void | Promise<void>
): Promise<TransactionResult>

Transaction Methods:

interface Transaction {
set(namespace: string, key: string, value: any): void;
get(namespace: string, key: string): any;
update(namespace: string, key: string, updater: Function): void;
delete(namespace: string, key: string): void;
}

Example:

// Transfer inventory between products (atomic)
await cortex.mutable.transaction(async (tx) => {
const qtyA = tx.get("inventory", "product-a");
const qtyB = tx.get("inventory", "product-b");

if (qtyA < 10) {
throw new Error("Insufficient inventory for product-a");
}

tx.update("inventory", "product-a", (qty) => qty - 10);
tx.update("inventory", "product-b", (qty) => qty + 10);

// Both updates or neither (ACID)
});

// Record sale and update inventory (atomic)
await cortex.mutable.transaction(async (tx) => {
tx.update("inventory", "widget-qty", (qty) => qty - 1);
tx.update("counters", "total-sales", (count) => (count || 0) + 1);
tx.update("counters", "revenue", (rev) => (rev || 0) + 49.99);
// All or nothing
});

Errors:

  • CortexError('TRANSACTION_FAILED') - Transaction rolled back
  • CortexError('TRANSACTION_TIMEOUT') - Exceeded time limit

Querying

list()

List keys in a namespace.

Signature:

cortex.mutable.list(
namespace: string,
filters?: MutableFilters
): Promise<MutableListResult>

Parameters:

interface MutableFilters {
keyPrefix?: string; // Keys starting with prefix
updatedAfter?: Date;
updatedBefore?: Date;
limit?: number;
offset?: number;
sortBy?: "key" | "updatedAt" | "accessCount";
sortOrder?: "asc" | "desc";
}

Example:

// List all inventory items
const items = await cortex.mutable.list("inventory", {
sortBy: "key",
limit: 100,
});

items.records.forEach((item) => {
console.log(`${item.key}: ${item.value}`);
});

// List specific prefix
const widgets = await cortex.mutable.list("inventory", {
keyPrefix: "widget-",
});

count()

Count keys in namespace.

Signature:

cortex.mutable.count(
namespace: string,
filters?: MutableFilters
): Promise<number>

Example:

// Total inventory items
const total = await cortex.mutable.count("inventory");

// Items updated today
const updated = await cortex.mutable.count("inventory", {
updatedAfter: new Date(Date.now() - 24 * 60 * 60 * 1000),
});

exists()

Check if key exists.

Signature:

cortex.mutable.exists(
namespace: string,
key: string
): Promise<boolean>

Example:

if (await cortex.mutable.exists("inventory", "widget-qty")) {
const qty = await cortex.mutable.get("inventory", "widget-qty");
} else {
// Initialize
await cortex.mutable.set("inventory", "widget-qty", 100);
}

Namespaces

Common Namespaces

inventory - Product quantities

await cortex.mutable.set("inventory", "product-x", 150);
await cortex.mutable.update("inventory", "product-x", (qty) => qty - 1);

config - Live configuration

await cortex.mutable.set(
"config",
"api-endpoint",
"https://api.example.com/v2",
);
await cortex.mutable.set("config", "max-retries", 3);

counters - Shared counters/metrics

await cortex.mutable.update("counters", "total-requests", (n) => (n || 0) + 1);
await cortex.mutable.update("counters", "active-users", (n) => n + 1);

docs - Live documentation references

await cortex.mutable.set("docs", "api-version", "v2.1.0");
await cortex.mutable.set(
"docs",
"changelog-url",
"https://docs.example.com/changelog",
);

state - Agent collaboration state

await cortex.mutable.set("state", "active-campaign-id", "camp-2025-q4");
await cortex.mutable.update("state", "agents-available", (list) => [
...list,
"agent-5",
]);

Complex Data Types

The Mutable Store supports any JSON-serializable value, not just primitives. You can store:

Storing Objects

// Store complex configuration object
await cortex.mutable.set("config", "email-settings", {
smtp: {
host: "smtp.example.com",
port: 587,
secure: true,
},
from: "noreply@example.com",
templates: {
welcome: "templates/welcome.html",
reset: "templates/reset.html",
},
});

// Retrieve and use
const emailConfig = await cortex.mutable.get("config", "email-settings");
sendEmail(emailConfig.smtp, emailConfig.from);

Storing Arrays

// Store product catalog
await cortex.mutable.set("inventory", "featured-products", [
{ id: "prod-1", name: "Widget A", price: 19.99, stock: 150 },
{ id: "prod-2", name: "Widget B", price: 29.99, stock: 75 },
{ id: "prod-3", name: "Widget C", price: 39.99, stock: 200 },
]);

// Atomically update array
await cortex.mutable.update("inventory", "featured-products", (products) => {
return products.map((p) =>
p.id === "prod-1" ? { ...p, stock: p.stock - 1 } : p,
);
});

Storing Nested Structures

// Store complete store configuration
await cortex.mutable.set("stores", "store-15", {
id: "store-15",
name: "Downtown Location",
address: {
street: "123 Main St",
city: "Springfield",
state: "IL",
zip: "62701",
},
hours: {
monday: { open: "09:00", close: "21:00" },
tuesday: { open: "09:00", close: "21:00" },
sunday: { open: "10:00", close: "18:00" },
},
departments: [
{ name: "Produce", manager: "Alice" },
{ name: "Dairy", manager: "Bob" },
{ name: "Bakery", manager: "Carol" },
],
metrics: {
revenue: 125000,
customersToday: 450,
averageTransaction: 67.5,
},
});

Size Considerations

// ✅ Good: Reasonable size (< 100KB recommended)
await cortex.mutable.set("config", "app-settings", {
theme: "dark",
language: "en",
notifications: true,
// ... dozens of settings
});

// ⚠️ Be careful: Very large objects (> 1MB)
await cortex.mutable.set("cache", "entire-product-catalog", {
// Thousands of products...
// Consider breaking into multiple keys instead
});

// ✅ Better: Split large datasets
await cortex.mutable.set("catalog", "page-1", products.slice(0, 100));
await cortex.mutable.set("catalog", "page-2", products.slice(100, 200));
await cortex.mutable.set("catalog", "page-3", products.slice(200, 300));

Hierarchical Data Patterns

For hierarchical/relational data like "Produce" → "Inventory" → "Grocery Store 15" → "Grocery Stores", Cortex provides several patterns:

Use delimiters in keys to create hierarchies:

// Pattern: namespace:key-with:delimiters
await cortex.mutable.set(
"grocery-stores",
"store-15:inventory:produce:apples",
{
quantity: 150,
unit: "lbs",
price: 2.99,
supplier: "Local Farms",
},
);

await cortex.mutable.set(
"grocery-stores",
"store-15:inventory:produce:bananas",
{
quantity: 200,
unit: "lbs",
price: 1.49,
supplier: "Tropical Import Co",
},
);

await cortex.mutable.set("grocery-stores", "store-15:inventory:dairy:milk", {
quantity: 50,
unit: "gallons",
price: 4.99,
supplier: "Dairy Fresh",
});

// Query all produce for store 15
const produce = await cortex.mutable.list("grocery-stores", {
keyPrefix: "store-15:inventory:produce:",
});

// Query all inventory for store 15
const allInventory = await cortex.mutable.list("grocery-stores", {
keyPrefix: "store-15:inventory:",
});

Benefits:

  • Easy prefix queries
  • Flat structure (no deep nesting)
  • Good performance
  • Easy to add/remove items

Pattern 2: Hierarchical Namespaces

Use delimiters in namespaces for organizational hierarchy:

// Pattern: hierarchical:namespace with flat keys
await cortex.mutable.set(
"grocery-stores:store-15:inventory",
"produce-apples",
{
quantity: 150,
price: 2.99,
},
);

await cortex.mutable.set(
"grocery-stores:store-15:inventory",
"produce-bananas",
{
quantity: 200,
price: 1.49,
},
);

await cortex.mutable.set(
"grocery-stores:store-15:metrics",
"daily-revenue",
12500,
);
await cortex.mutable.set(
"grocery-stores:store-15:metrics",
"customer-count",
450,
);

// Query entire namespace
const storeInventory = await cortex.mutable.list(
"grocery-stores:store-15:inventory",
);
const storeMetrics = await cortex.mutable.list(
"grocery-stores:store-15:metrics",
);

Benefits:

  • Logical namespace organization
  • Clear separation of concerns
  • Easy to list all keys in a category

Store hierarchy as nested object in value:

// Store entire store hierarchy in one key
await cortex.mutable.set("grocery-stores", "store-15", {
id: "store-15",
name: "Downtown Location",
inventory: {
produce: {
apples: { quantity: 150, price: 2.99, unit: "lbs" },
bananas: { quantity: 200, price: 1.49, unit: "lbs" },
oranges: { quantity: 100, price: 3.49, unit: "lbs" },
},
dairy: {
milk: { quantity: 50, price: 4.99, unit: "gallons" },
cheese: { quantity: 75, price: 6.99, unit: "lbs" },
},
bakery: {
bread: { quantity: 100, price: 3.99, unit: "loaves" },
donuts: { quantity: 200, price: 1.99, unit: "each" },
},
},
metrics: {
revenue: 12500,
customers: 450,
avgTransaction: 27.78,
},
});

// Update specific nested value atomically
await cortex.mutable.update("grocery-stores", "store-15", (store) => ({
...store,
inventory: {
...store.inventory,
produce: {
...store.inventory.produce,
apples: {
...store.inventory.produce.apples,
quantity: store.inventory.produce.apples.quantity - 10,
},
},
},
}));

// Or use helper function
async function updateNestedInventory(
storeId: string,
department: string,
item: string,
updates: Partial<any>,
) {
await cortex.mutable.update("grocery-stores", storeId, (store) => ({
...store,
inventory: {
...store.inventory,
[department]: {
...store.inventory[department],
[item]: {
...store.inventory[department][item],
...updates,
},
},
},
}));
}

await updateNestedInventory("store-15", "produce", "apples", { quantity: 140 });

Benefits:

  • Single atomic update for related data
  • Natural data representation
  • Smaller transaction scope
  • Easy to retrieve entire hierarchy

Trade-offs:

  • Large objects can be slow to update
  • Need careful atomic updates
  • Harder to query subsets

Combine approaches for optimal performance:

// Store metadata in nested object
await cortex.mutable.set("grocery-stores", "store-15-meta", {
id: "store-15",
name: "Downtown Location",
address: { street: "123 Main St", city: "Springfield" },
departments: ["produce", "dairy", "bakery"],
});

// Store inventory items individually with hierarchical keys
await cortex.mutable.set("inventory", "store-15:produce:apples", {
quantity: 150,
price: 2.99,
unit: "lbs",
});

await cortex.mutable.set("inventory", "store-15:produce:bananas", {
quantity: 200,
price: 1.49,
unit: "lbs",
});

// Store aggregated metrics separately
await cortex.mutable.set("metrics", "store-15-daily", {
revenue: 12500,
customers: 450,
date: new Date().toISOString(),
});

Benefits:

  • Optimal for large datasets
  • Fast queries and updates
  • Good separation of concerns
  • Scalable architecture

Helper Functions for Hierarchical Data

// Parse hierarchical keys
function parseKey(key: string): string[] {
return key.split(":");
}

function buildKey(...parts: string[]): string {
return parts.join(":");
}

// Example usage
const key = buildKey("store-15", "inventory", "produce", "apples");
await cortex.mutable.set("grocery-stores", key, { quantity: 150 });

const parts = parseKey(key); // ["store-15", "inventory", "produce", "apples"]
console.log(`Store: ${parts[0]}, Department: ${parts[2]}, Item: ${parts[3]}`);

// Query helpers
async function listByPrefix(namespace: string, ...prefixParts: string[]) {
const prefix = buildKey(...prefixParts);
return await cortex.mutable.list(namespace, {
keyPrefix: prefix,
});
}

// Get all produce for store 15
const produce = await listByPrefix(
"grocery-stores",
"store-15",
"inventory",
"produce",
);

// Get all inventory for store 15
const inventory = await listByPrefix("grocery-stores", "store-15", "inventory");

Choosing the Right Pattern

PatternBest ForQuery PerformanceUpdate PerformanceComplexity
Hierarchical KeysPrefix queries, flat relationships⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐Low
Hierarchical NamespacesOrganizational structure⭐⭐⭐⭐⭐⭐⭐⭐⭐Low
Nested Object ValuesRelated data, small datasets⭐⭐⭐⭐⭐⭐Medium
HybridLarge systems, mixed access⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐Medium

Recommendations:

  • Inventory systems: Hierarchical Keys or Hybrid
  • Configuration: Nested Object Values
  • Metrics/Analytics: Hierarchical Keys
  • Multi-tenant data: Hierarchical Namespaces
  • Small datasets (< 100 items): Nested Object Values
  • Large datasets (> 1000 items): Hierarchical Keys + Hybrid

Complete Real-World Example: Grocery Store Chain

Here's a comprehensive example using the Hybrid pattern for a multi-store inventory system:

// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Setup: Initialize grocery store chain
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

// Store chain metadata (nested object - small, related data)
await cortex.mutable.set("chain", "metadata", {
name: "Fresh Foods Chain",
totalStores: 25,
headquarters: "Springfield, IL",
storeIds: ["store-1", "store-15", "store-23"],
});

// Store individual store configs (nested object per store)
await cortex.mutable.set("stores", "store-15", {
id: "store-15",
name: "Downtown Location",
address: "123 Main St, Springfield",
manager: "Alice Johnson",
departments: ["produce", "dairy", "bakery", "meat"],
openHours: { open: "09:00", close: "21:00" },
});

// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Inventory: Hierarchical keys (scalable, fast queries)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

// Set initial inventory (hierarchical keys: store:dept:item)
await cortex.mutable.set("inventory", "store-15:produce:apples", {
quantity: 150,
unit: "lbs",
price: 2.99,
supplier: "Local Farms",
lastRestocked: new Date(),
});

await cortex.mutable.set("inventory", "store-15:produce:bananas", {
quantity: 200,
unit: "lbs",
price: 1.49,
supplier: "Tropical Import",
});

await cortex.mutable.set("inventory", "store-15:dairy:milk", {
quantity: 50,
unit: "gallons",
price: 4.99,
supplier: "Dairy Fresh",
});

// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Operations: Customer purchases (atomic updates)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

// Customer buys 10 lbs of apples
await cortex.mutable.update("inventory", "store-15:produce:apples", (item) => {
if (item.quantity < 10) {
throw new Error("Insufficient stock");
}
return {
...item,
quantity: item.quantity - 10,
};
});

// Multi-item purchase (ACID transaction)
await cortex.mutable.transaction(async (tx) => {
// Buy apples, bananas, and milk
tx.update("inventory", "store-15:produce:apples", (item) => ({
...item,
quantity: item.quantity - 5,
}));

tx.update("inventory", "store-15:produce:bananas", (item) => ({
...item,
quantity: item.quantity - 3,
}));

tx.update("inventory", "store-15:dairy:milk", (item) => ({
...item,
quantity: item.quantity - 2,
}));

// Update store metrics
tx.update("metrics", "store-15-daily", (metrics) => ({
...metrics,
sales: (metrics?.sales || 0) + 1,
revenue: (metrics?.revenue || 0) + 24.43, // Total purchase
}));

// All updates succeed together or fail together
});

// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Queries: Get inventory by hierarchy level
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

// Get all produce for store 15
const produce = await cortex.mutable.list("inventory", {
keyPrefix: "store-15:produce:",
});

console.log(`Store 15 produce items: ${produce.records.length}`);
produce.records.forEach((item) => {
console.log(
`- ${item.key.split(":")[2]}: ${item.value.quantity} ${item.value.unit}`,
);
});

// Get ALL inventory for store 15
const allInventory = await cortex.mutable.list("inventory", {
keyPrefix: "store-15:",
});

// Get specific item across all stores
const allApples = await cortex.mutable.list("inventory", {
keyPrefix: "store-", // Gets all stores
});
const applesOnly = allApples.records.filter((r) => r.key.endsWith(":apples"));

// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Aggregation: Chain-wide reports
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

// Get total inventory across all stores
async function getChainwideInventory(department?: string) {
const allStores = await cortex.mutable.get("chain", "metadata");

const inventory: Record<string, number> = {};

for (const storeId of allStores.storeIds) {
const prefix = department ? `${storeId}:${department}:` : `${storeId}:`;

const items = await cortex.mutable.list("inventory", {
keyPrefix: prefix,
});

items.records.forEach((record) => {
const itemName = record.key.split(":").pop()!;
inventory[itemName] = (inventory[itemName] || 0) + record.value.quantity;
});
}

return inventory;
}

// Total produce across all stores
const chainProduce = await getChainwideInventory("produce");
console.log("Chain-wide produce:", chainProduce);
// { apples: 3750, bananas: 5000, oranges: 2100 }

// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Restocking: Bulk updates
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

// Restock all produce for store 15
async function restockDepartment(
storeId: string,
department: string,
items: Record<string, number>,
) {
await cortex.mutable.transaction(async (tx) => {
for (const [itemName, quantity] of Object.entries(items)) {
const key = `${storeId}:${department}:${itemName}`;

tx.update("inventory", key, (current) => ({
...current,
quantity: current.quantity + quantity,
lastRestocked: new Date(),
}));
}
});
}

await restockDepartment("store-15", "produce", {
apples: 100,
bananas: 150,
oranges: 75,
});

// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Low Stock Alerts
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

async function checkLowStock(storeId: string, threshold: number = 20) {
const inventory = await cortex.mutable.list("inventory", {
keyPrefix: `${storeId}:`,
});

const lowStock = inventory.records.filter(
(item) => item.value.quantity < threshold,
);

if (lowStock.length > 0) {
console.log(`⚠️ Low stock alert for ${storeId}:`);
lowStock.forEach((item) => {
const [store, dept, product] = item.key.split(":");
console.log(
` - ${dept}/${product}: ${item.value.quantity} ${item.value.unit} remaining`,
);
});
}

return lowStock;
}

await checkLowStock("store-15", 25);

This example demonstrates:

  • ✅ Complex objects as values (store metadata, inventory items)
  • ✅ Hierarchical keys for scalable queries (store:dept:item)
  • ✅ Nested objects for related configuration
  • ✅ Atomic transactions across multiple keys
  • ✅ Prefix-based queries for hierarchy traversal
  • ✅ Aggregation across hierarchies
  • ✅ Real-world business logic (purchases, restocking, alerts)

Best Practices

1. Use Descriptive Namespaces

// ✅ Good namespaces
"inventory";
"config-production";
"counters-analytics";
"docs-api";

// ❌ Bad namespaces
"data";
"stuff";
"temp";

2. Atomic Operations Only

// ❌ DON'T: Race condition
const current = await cortex.mutable.get("inventory", "widget-qty");
await cortex.mutable.set("inventory", "widget-qty", current - 1);
// Another agent could modify between get and set!

// ✅ DO: Atomic update
await cortex.mutable.update("inventory", "widget-qty", (qty) => qty - 1);
// ACID guarantees no race condition

3. Initialize Before Use

// Safe initialization
async function getOrInitialize(
namespace: string,
key: string,
defaultValue: any,
) {
const value = await cortex.mutable.get(namespace, key);
if (value === null) {
await cortex.mutable.set(namespace, key, defaultValue);
return defaultValue;
}
return value;
}

const qty = await getOrInitialize("inventory", "new-product", 0);

4. Use Transactions for Multi-Key Operations

// ✅ Atomic multi-key update
await cortex.mutable.transaction(async (tx) => {
tx.update("inventory", "product-a", (qty) => qty - 1);
tx.update("counters", "total-sales", (n) => n + 1);
// Both or neither
});

// ❌ Non-atomic (could fail partway)
await cortex.mutable.update("inventory", "product-a", (qty) => qty - 1);
await cortex.mutable.update("counters", "total-sales", (n) => n + 1);
// Second could fail, leaving inconsistent state

5. Choose Appropriate Data Structure

Use Complex Objects When:

// ✅ Related data that changes together
await cortex.mutable.set("stores", "store-15", {
name: "Downtown",
manager: "Alice",
address: { street: "123 Main", city: "Springfield" },
// All fields updated as a unit
});

// ✅ Small datasets (< 100 items)
await cortex.mutable.set("config", "app-settings", {
theme: "dark",
language: "en",
features: { chat: true, voice: false },
});

// ✅ Configuration that's read together
await cortex.mutable.set("email", "settings", {
smtp: { host: "...", port: 587 },
templates: { welcome: "...", reset: "..." },
});

Use Hierarchical Keys When:

// ✅ Large datasets (> 100 items)
await cortex.mutable.set("inventory", "store-15:produce:apples", {...});
await cortex.mutable.set("inventory", "store-15:produce:bananas", {...});
// Can have thousands of products

// ✅ Need prefix queries
const produce = await cortex.mutable.list("inventory", {
keyPrefix: "store-15:produce:",
});

// ✅ Independent updates (no related data)
await cortex.mutable.update("inventory", "store-15:dairy:milk", ...);
// Doesn't affect produce or other items

// ✅ Different access patterns
const apples = await cortex.mutable.get("inventory", "store-15:produce:apples");
// Fast direct access to single item

Decision Matrix:

ScenarioRecommended PatternWhy
Store metadata (< 50 fields)Complex ObjectRelated data, atomic updates
Product catalog (1000s of items)Hierarchical KeysScalable, independent updates
App configurationComplex ObjectRead/updated together
Multi-tenant inventoryHierarchical KeysPer-tenant prefix queries
Workflow stateComplex ObjectState transitions are atomic
Metrics/countersHierarchical KeysIndependent increments
Small lists (< 100 items)Complex Object (array)Simple, atomic
Large lists (> 1000 items)Hierarchical KeysScalable queries

6. Use TypeScript for Type Safety

Define interfaces for complex objects:

// Define your data structures
interface InventoryItem {
quantity: number;
unit: string;
price: number;
supplier: string;
sku: string;
lastRestocked: Date;
}

interface StoreConfig {
id: string;
name: string;
manager: string;
address: {
street: string;
city: string;
state: string;
zip: string;
};
departments: string[];
openHours: {
open: string;
close: string;
};
}

// Type-safe operations
async function setInventoryItem(
storeId: string,
department: string,
item: string,
data: InventoryItem,
): Promise<void> {
const key = `${storeId}:${department}:${item}`;
await cortex.mutable.set("inventory", key, data);
}

async function getInventoryItem(
storeId: string,
department: string,
item: string,
): Promise<InventoryItem | null> {
const key = `${storeId}:${department}:${item}`;
return await cortex.mutable.get("inventory", key);
}

// Usage is type-safe
await setInventoryItem("store-15", "produce", "apples", {
quantity: 150,
unit: "lbs",
price: 2.99,
supplier: "Local Farms",
sku: "PROD-APL-001",
lastRestocked: new Date(),
});

const apples = await getInventoryItem("store-15", "produce", "apples");
if (apples) {
console.log(apples.quantity); // TypeScript knows this is a number
// apples.invalidField // TypeScript error!
}

Integration with Vector Layer

Mutable data CAN be referenced by Vector memories (as snapshots):

// 1. Store mutable data
await cortex.mutable.set(
"config",
"api-endpoint",
"https://api.example.com/v2",
);

// 2. Create Vector memory referencing it (snapshot)
await cortex.vector.store("config-agent", {
content: "API endpoint changed to v2",
contentType: "raw",
source: { type: "system", timestamp: new Date() },
mutableRef: {
namespace: "config",
key: "api-endpoint",
snapshotValue: "https://api.example.com/v2", // Value at time of indexing
snapshotAt: new Date(),
},
metadata: {
importance: 80,
tags: ["config", "api"],
},
});

// 3. Later, mutable value changes
await cortex.mutable.set(
"config",
"api-endpoint",
"https://api.example.com/v3",
);

// Vector memory still has snapshot of v2
// Current value is v3 (from mutable store)

Note: Vector's mutableRef is a snapshot, not live. Use for audit/history, not current values.


Purging

purge()

Delete a single key.

Signature:

cortex.mutable.purge(
namespace: string,
key: string
): Promise<PurgeResult>

Example:

const result = await cortex.mutable.purge("inventory", "discontinued-product");
console.log(`Deleted: ${result.deleted}`);

purgeNamespace()

Delete entire namespace.

Signature:

cortex.mutable.purgeNamespace(
namespace: string,
options?: PurgeOptions
): Promise<PurgeResult>

Example:

// Delete all test data
const result = await cortex.mutable.purgeNamespace("test-data");
console.log(`Deleted ${result.keysDeleted} keys`);

// Preview first
const preview = await cortex.mutable.purgeNamespace("old-config", {
dryRun: true,
});
console.log(`Would delete ${preview.keysDeleted} keys`);

purgeMany()

Delete keys matching filters.

Signature:

cortex.mutable.purgeMany(
namespace: string,
filters: MutableFilters
): Promise<PurgeResult>

Example:

// Delete old keys
await cortex.mutable.purgeMany("cache", {
updatedBefore: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
});

// Delete inactive keys
await cortex.mutable.purgeMany("state", {
lastAccessedBefore: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000),
});

Use Cases

Use Case 1: Live Inventory (Complex Hierarchical Data)

// Initialize inventory with complex objects and hierarchical keys
await cortex.mutable.set("inventory", "store-15:produce:apples", {
quantity: 150,
unit: "lbs",
price: 2.99,
supplier: "Local Farms",
sku: "PROD-APL-001",
perishable: true,
expiryDate: new Date("2025-10-30"),
});

// Customer orders 10 lbs (atomic decrement with complex object)
await cortex.mutable.update("inventory", "store-15:produce:apples", (item) => {
if (item.quantity < 10) throw new Error("Out of stock");

return {
...item,
quantity: item.quantity - 10,
lastSold: new Date(),
};
});

// Check availability with full details
const apples = await cortex.mutable.get("inventory", "store-15:produce:apples");
console.log(`${apples.quantity} ${apples.unit} available at $${apples.price}`);

// Restock (atomic increment + update metadata)
await cortex.mutable.update("inventory", "store-15:produce:apples", (item) => ({
...item,
quantity: item.quantity + 50,
lastRestocked: new Date(),
supplier: "Local Farms", // Confirm supplier
}));

// Query all produce for this store
const allProduce = await cortex.mutable.list("inventory", {
keyPrefix: "store-15:produce:",
});

console.log(`Store 15 has ${allProduce.total} produce items`);

Use Case 2: Live Configuration (Complex Nested Objects)

// Set application configuration as nested object
await cortex.mutable.set("config", "app-settings", {
api: {
endpoint: "https://api.example.com/v1",
timeout: 30000,
retries: 3,
rateLimits: {
perMinute: 60,
perHour: 1000,
},
},
features: {
enableChat: true,
enableVoice: false,
maxConcurrentChats: 5,
enableFileUpload: true,
},
notifications: {
email: true,
sms: false,
push: true,
channels: ["email", "push"],
},
security: {
sessionTimeout: 3600,
requireMFA: false,
allowedOrigins: ["https://app.example.com"],
},
});

// Agents check config (type-safe access)
const config = await cortex.mutable.get("config", "app-settings");
if (
config.features.enableChat &&
activeChats < config.features.maxConcurrentChats
) {
// Accept new chat
}

// Update specific nested value (atomic)
await cortex.mutable.update("config", "app-settings", (cfg) => ({
...cfg,
features: {
...cfg.features,
maxConcurrentChats: 10, // Immediate effect for all agents
},
}));

// Or update API endpoint
await cortex.mutable.update("config", "app-settings", (cfg) => ({
...cfg,
api: {
...cfg.api,
endpoint: "https://api.example.com/v2",
timeout: 60000, // Also increase timeout
},
}));

Use Case 3: Shared Counters

// Initialize counters
await cortex.mutable.set("counters", "total-requests", 0);
await cortex.mutable.set("counters", "successful-requests", 0);
await cortex.mutable.set("counters", "failed-requests", 0);

// Each agent increments (atomic)
await cortex.mutable.update("counters", "total-requests", (n) => n + 1);

if (requestSucceeded) {
await cortex.mutable.update("counters", "successful-requests", (n) => n + 1);
} else {
await cortex.mutable.update("counters", "failed-requests", (n) => n + 1);
}

// Get stats
const total = await cortex.mutable.get("counters", "total-requests");
const success = await cortex.mutable.get("counters", "successful-requests");
console.log(`Success rate: ${((success / total) * 100).toFixed(1)}%`);

Use Case 4: Agent Collaboration State (Complex Workflow)

// Track active workflow with complex state
await cortex.mutable.set("workflows", "refund-request-12345", {
id: "refund-request-12345",
type: "refund",
status: "in-progress",
createdAt: new Date(),
customer: {
id: "user-123",
name: "Alex Johnson",
email: "alex@example.com",
},
order: {
orderId: "ORD-98765",
amount: 299.99,
items: ["Widget A", "Widget B"],
purchaseDate: new Date("2025-10-15"),
},
workflow: {
currentStep: "manager-approval",
completedSteps: ["initiated", "customer-verified"],
pendingSteps: ["manager-approval", "payment-processing", "complete"],
},
agents: {
initiator: "support-agent-1",
currentAssignee: "manager-agent",
history: [
{
agentId: "support-agent-1",
action: "initiated",
timestamp: new Date(),
},
{ agentId: "support-agent-1", action: "verified", timestamp: new Date() },
],
},
notes: ["Customer reports defective product", "Original packaging intact"],
});

// Manager approves (atomic state transition)
await cortex.mutable.update(
"workflows",
"refund-request-12345",
(workflow) => ({
...workflow,
status: "approved",
workflow: {
...workflow.workflow,
currentStep: "payment-processing",
completedSteps: [...workflow.workflow.completedSteps, "manager-approval"],
pendingSteps: workflow.workflow.pendingSteps.filter(
(s) => s !== "manager-approval",
),
},
agents: {
...workflow.agents,
currentAssignee: "finance-agent",
history: [
...workflow.agents.history,
{ agentId: "manager-agent", action: "approved", timestamp: new Date() },
],
},
approvalDetails: {
approvedBy: "manager-agent",
approvedAt: new Date(),
amount: 299.99,
},
}),
);

// Finance agent processes payment
await cortex.mutable.update("workflows", "refund-request-12345", (workflow) => {
if (workflow.status !== "approved") {
throw new Error("Workflow not approved");
}

return {
...workflow,
status: "completed",
workflow: {
...workflow.workflow,
currentStep: "complete",
completedSteps: [
...workflow.workflow.completedSteps,
"payment-processing",
],
pendingSteps: [],
},
completedAt: new Date(),
};
});

Performance

Optimized for Frequent Updates

// Mutable store is designed for high-frequency updates
for (let i = 0; i < 1000; i++) {
await cortex.mutable.update("counters", "page-views", (n) => n + 1);
}
// Fast! No versioning overhead

Caching Recommendations

// For frequently-read, rarely-updated values
class MutableCache {
private cache = new Map<string, { value: any; cachedAt: Date }>();
private ttl = 60000; // 60 second cache

async get(namespace: string, key: string) {
const cacheKey = `${namespace}:${key}`;
const cached = this.cache.get(cacheKey);

if (cached && Date.now() - cached.cachedAt.getTime() < this.ttl) {
return cached.value;
}

const value = await cortex.mutable.get(namespace, key);
this.cache.set(cacheKey, { value, cachedAt: new Date() });
return value;
}
}

Limitations

No Version History

// Version 1
await cortex.mutable.set("config", "timeout", 30);

// Version 2 (v1 is GONE)
await cortex.mutable.set("config", "timeout", 60);

// Can't retrieve v1 - it's overwritten!
const current = await cortex.mutable.get("config", "timeout");
console.log(current); // 60 only

// If you need history, use immutable store instead!

Backup Strategy

// Snapshot mutable data for backups
async function backupMutableNamespace(namespace: string) {
const all = await cortex.mutable.list(namespace);

const backup = {
namespace,
timestamp: new Date(),
data: all.records.reduce(
(acc, record) => {
acc[record.key] = record.value;
return acc;
},
{} as Record<string, any>,
),
};

// Store backup (could use immutable store or external storage)
await cortex.immutable.store({
type: "mutable-backup",
id: `${namespace}-backup-${Date.now()}`,
data: backup,
});

return backup;
}

// Restore from backup
async function restoreMutableNamespace(backupId: string) {
const backup = await cortex.immutable.get("mutable-backup", backupId);

for (const [key, value] of Object.entries(backup.data.data)) {
await cortex.mutable.set(backup.data.namespace, key, value);
}
}

Graph-Lite Capabilities

Mutable records can participate in the graph structure via userId:

Mutable Record as Graph Node:

  • Represents live/current state data
  • Can be linked to users for GDPR compliance

Edges:

  • userId to User (optional, for user-specific data)
  • mutableRef from Memories (snapshots of mutable state)

Graph Pattern:

// User → Mutable Records (via userId)
const userSessions = await cortex.mutable.list('user-sessions', {
userId: 'user-123'
});

const userPreferences = await cortex.mutable.get('user-preferences', 'user-123-prefs');

// Memory → Mutable (via mutableRef - snapshot)
await cortex.vector.store('agent-1', {
content: 'Config changed to value X',
mutableRef: {
namespace: 'config',
key: 'api-endpoint',
snapshotValue: 'https://api.example.com/v2',
snapshotAt: new Date()
}
});

// Graph:
User-123
└──[userId]──> Mutable: user-sessions/session-abc
└──[userId]──> Mutable: user-cache/cache-xyz

Memory-def
└──[mutableRef]──> Mutable: config/api-endpoint (snapshot at time T)

Note: Mutable data changes frequently, so mutableRef stores snapshots (value at time of reference), not live links.

GDPR: Mutable records with userId are included in cascade deletion traversal.

Learn more: Graph-Lite Traversal


Summary

Mutable Store provides:

  • ✅ Shared live data across agents
  • ✅ ACID transaction guarantees
  • ✅ Current-value semantics (fast)
  • ✅ No version overhead
  • ✅ Atomic updates (no race conditions)

Use for:

  • Real-time inventory
  • Live configuration
  • Shared counters/metrics
  • Agent collaboration state
  • Frequently-changing reference data

Don't use for:

  • Data needing version history (use cortex.immutable.*)
  • Private conversations (use cortex.conversations.*)
  • Searchable knowledge (use cortex.vector.* + immutable)
  • Audit trails (use cortex.immutable.* for versioning)

Next Steps


Questions? Ask in GitHub Discussions or Discord.