Choosing Your Memory Schema
By Arc Labs Research8 min read
The Problem: Choosing the Wrong Memory Type
Agents fail quietly when memory types don't match use cases:
- Storing temporal events as facts: "User browsed shoes on 2026-05-01" stored as a fact, but you lose the timestamp. Later, you can't answer "What did user do last week?"
- Storing relationships as facts: "User owns product X" stored as
{ content: "owns product X" }, but queries miss it because it's worded differently elsewhere ("User purchased X", "owns item X"). - Mixing types: Some code treats memory as immutable facts, other code updates it. Consistency breaks.
- No filtering strategy: All queries are semantic search. You pay for expensive embeddings instead of using metadata filters.
Production agents need a schema that matches query patterns and minimizes retrieval cost.
Memory Type Decision Tree
START: What are your primary queries?
1. "What are the user's static facts?"
├─ Yes → USE FACT
│ (Example: "User prefers Nike", "Has budget <$500")
│
└─ No, next question
2. "Do you need temporal sequence or causality?"
├─ Yes → USE EVENT
│ (Example: "User browsed shoes at 10am, bought at 2pm")
│
└─ No, next question
3. "Do you model the world as interconnected entities + their attributes?"
├─ Yes → USE ENTITY
│ (Example: Customer + Orders + Products + Preferences, interconnected)
│
└─ No, next question
4. "Do you need to query relationships between pairs of things?"
├─ Yes → USE RELATION
│ (Example: "What is the relationship between user-123 and product-456?")
│
└─ You don't fit the schema yet—consult docsFour Memory Types: Comparison Table
| Type | Content | Example | Retrieval Pattern | Cost | When to Use |
|---|---|---|---|---|---|
| Fact | Stateless, single-valued or categorical | "Customer prefers Nike", "Has budget 500" | Keyword (metadata) + semantic | Low | Static user preferences, category tags, configuration |
| Event | Temporal, causally linked, sequence matters | "Clicked product at 10:00am", "Purchased boots at 2pm" | Temporal (sort by date) + semantic | Medium | User actions, conversation history, time-series events |
| Entity | Rich object with typed attributes + relationships | Customer { name, email, preferences[], orders[], location } | Graph traversal + semantic | High | Complex domains (customer + orders + products, all interconnected) |
| Relation | Binary relationship between two entities | (user-123) --"purchased"--> (product-456), with metadata | Relationship query ("What connects X and Y?") | Medium | Knowledge graphs, recommendation (who likes what?) |
Type 1: Fact (Static Attributes)
Use facts for information that doesn't change or changes rarely.
Schema
interface FactMemory {
agentId: string;
namespace: string;
content: string; // "Customer prefers Nike"
type: "fact";
metadata: {
category: string; // "preference", "configuration", "setting"
value?: string; // "Nike" (optional, for filtering)
strength?: "strong" | "weak" | "neutral"; // Inferred from frequency
timestamp: string; // When this fact was observed
};
}Retrieval Pattern
import { Brain } from "brain-ai";
const memory = new Brain({ namespace: "customer-service" });
// Query 1: Get all facts for a customer (keyword)
const facts = await memory.retrieve({
agentId: "customer-12345",
filters: { type: "fact" }, // Metadata filter (cheap)
topK: 20,
});
// Query 2: Get preference facts only (metadata filter)
const preferences = await memory.retrieve({
agentId: "customer-12345",
filters: { type: "fact", "metadata.category": "preference" },
topK: 10,
});
// Query 3: Strong preferences (filtered)
const strongPrefs = await memory.retrieve({
agentId: "customer-12345",
filters: { type: "fact", "metadata.strength": "strong" },
topK: 5,
});
// Use the results
const preferredBrands = preferences
.map(f => f.metadata.value)
.filter(Boolean);
// ["Nike", "Adidas"] (brands with strong preference)When to Use
- Customer preferences (brand, price range, size, color)
- Configuration (notification settings, language, timezone)
- Tags or categories (VIP customer, at-risk churn, high-value)
When NOT to Use
- Temporal sequences (use Event instead)
- Relationships between entities (use Relation instead)
Type 2: Event (Temporal Sequence)
Use events for ordered actions with timestamps.
Schema
interface EventMemory {
agentId: string;
namespace: string;
content: string; // "User clicked product page"
type: "event";
metadata: {
timestamp: string; // ISO 8601, required for sorting
action: string; // "click", "purchase", "view", "message"
entityId?: string; // Product ID, order ID, etc.
entityType?: string; // "product", "order", "page"
outcome?: string; // "success", "failed", "abandoned"
context?: {
sessionId?: string;
source?: string; // "web", "mobile", "api"
};
};
}Retrieval Pattern
// Query 1: Get recent events (temporal, sorted by timestamp)
const recentEvents = await memory.retrieve({
agentId: "customer-12345",
filters: { type: "event" },
topK: 10,
sortBy: "timestamp", // Retrieve most recent first
direction: "desc",
});
// Query 2: Get events in time window
const lastWeekEvents = await memory.retrieve({
agentId: "customer-12345",
filters: {
type: "event",
"metadata.timestamp": { $gte: "2026-05-06T00:00:00Z" },
},
topK: 50,
});
// Query 3: Get events of specific action
const purchases = await memory.retrieve({
agentId: "customer-12345",
filters: { type: "event", "metadata.action": "purchase" },
topK: 20,
});
// Query 4: Semantic search on events
const shoeBrowsing = await memory.retrieve({
agentId: "customer-12345",
query: "browsing shoes or boots",
filters: { type: "event" },
topK: 5,
});
// Use the results
const timeline = recentEvents.map(e => ({
time: e.metadata.timestamp,
action: e.metadata.action,
entity: e.metadata.entityId,
}));
// Reconstruct sequence: clicked product -> added to cart -> purchasedWhen to Use
- Conversation history (messages, turn sequence)
- User actions (clicks, purchases, logins, errors)
- Time-series data (sensor readings with timestamps)
- Audit logs (who did what when)
When NOT to Use
- Static attributes (use Fact instead)
- Complex object relationships (use Entity instead)
Type 3: Entity (Rich Objects with Relationships)
Use entities when you model a domain as interconnected objects.
Schema
interface EntityMemory {
agentId: string;
namespace: string;
content: string; // JSON serialized or prose description
type: "entity";
metadata: {
entityId: string; // Unique ID (customer-123, product-456)
entityType: string; // "customer", "order", "product", "location"
// Entity attributes (use for filtering and updates)
attributes?: {
name?: string;
category?: string;
status?: string;
[key: string]: any;
};
// Links to related entities
relations?: {
customer?: string[]; // Related customer IDs
products?: string[]; // Related product IDs
orders?: string[]; // Related order IDs
};
timestamp: string;
};
}
// Example: Customer entity
const customerEntity = {
agentId: "agent-ecommerce",
content: `
Customer Sarah Chen (sarah@example.com)
- Account created 2026-01-15
- Tier: Gold (lifetime spend >$5000)
- Preferences: Nike shoes, budget $100–$300, size 8
- Recent orders: [order-789, order-790]
- Associated reviews: [review-123, review-124]
`,
type: "entity",
metadata: {
entityId: "customer-sarah-123",
entityType: "customer",
attributes: {
name: "Sarah Chen",
email: "sarah@example.com",
accountCreated: "2026-01-15",
tier: "Gold",
lifetimeSpend: 5500,
},
relations: {
orders: ["order-789", "order-790", "order-656"],
reviews: ["review-123", "review-124"],
referrals: ["customer-alex-456"],
},
},
};Retrieval Pattern
// Query 1: Get entity by ID
const customer = await memory.retrieve({
agentId: "agent-ecommerce",
filters: { type: "entity", "metadata.entityId": "customer-sarah-123" },
topK: 1,
});
// Query 2: Expand relationships (get related entities)
if (customer.length > 0) {
const relatedOrderIds = customer[0].metadata.relations?.orders || [];
const orders = await memory.retrieve({
agentId: "agent-ecommerce",
filters: {
type: "entity",
"metadata.entityId": { $in: relatedOrderIds },
},
});
}
// Query 3: Search by attribute
const goldTierCustomers = await memory.retrieve({
agentId: "agent-ecommerce",
filters: {
type: "entity",
"metadata.entityType": "customer",
"metadata.attributes.tier": "Gold",
},
topK: 50,
});
// Query 4: Semantic search + attribute filter
const nikeFans = await memory.retrieve({
agentId: "agent-ecommerce",
query: "customers who prefer Nike shoes",
filters: {
type: "entity",
"metadata.entityType": "customer",
},
topK: 20,
});When to Use
- Recommendation systems (customers + products + reviews, all interconnected)
- Knowledge bases (entities + attributes + relationships modeled together)
- CRM systems (customers, accounts, contacts, interactions)
- Complex domains where you traverse relationships frequently
When NOT to Use
- Simple, flat data (use Fact instead)
- Temporal sequences (use Event instead)
- You only have 1–2 entity types (use simpler types)
Type 4: Relation (Binary Relationships)
Use relations for first-order relationships between pairs of things.
Schema
interface RelationMemory {
agentId: string;
namespace: string;
content: string; // "Sarah purchased a Nike Air Jordan 1"
type: "relation";
metadata: {
subject: string; // "customer-sarah-123"
subjectType: string; // "customer"
predicate: string; // "purchased", "reviewed", "recommended", "follows"
object: string; // "product-nike-aj1-456"
objectType: string; // "product"
strength?: number; // 0.0–1.0, confidence/weight of relationship
timestamp: string; // When relationship was formed
// Optional: relationship details
context?: {
reviewScore?: 5; // If predicate is "reviewed"
quantity?: 1; // If predicate is "purchased"
reason?: "recommendation_engine"; // Why this relationship?
};
};
}Retrieval Pattern
// Query 1: What did Sarah purchase?
const sarahPurchases = await memory.retrieve({
agentId: "agent-ecommerce",
filters: {
type: "relation",
"metadata.subject": "customer-sarah-123",
"metadata.predicate": "purchased",
},
topK: 20,
});
// Query 2: Who purchased this product?
const aj1Buyers = await memory.retrieve({
agentId: "agent-ecommerce",
filters: {
type: "relation",
"metadata.object": "product-nike-aj1-456",
"metadata.predicate": "purchased",
},
topK: 100,
});
// Query 3: High-confidence relationships only
const strongRelations = await memory.retrieve({
agentId: "agent-ecommerce",
filters: {
type: "relation",
"metadata.subject": "customer-sarah-123",
"metadata.strength": { $gte: 0.8 }, // Only strong relationships
},
topK: 10,
});
// Query 4: Semantic search for implicit relationships
const similarTaste = await memory.retrieve({
agentId: "agent-ecommerce",
query: "customers with similar purchase history",
filters: {
type: "relation",
"metadata.predicate": "purchased",
},
topK: 50,
});When to Use
- Recommendations (who-bought-what, who-reviewed-what)
- Social networks (follows, friends, collaborates-with)
- Knowledge graphs (simple facts about connections)
- Audit trails (user-performed-action, system-generated-event)
When NOT to Use
- Static attributes (use Fact instead)
- Many-to-many relationships with complex logic (use Entity instead)
Schema Design Decision Checklist
- Identify query patterns: What are your 5 most common queries? (e.g., "Get customer preferences", "What did user do last week?")
- Map to memory types: Does each query fit fact, event, entity, or relation?
- Test schema compatibility: Can you answer all 5 queries efficiently with your chosen schema?
- Design for filtering: Which fields should be indexed for fast metadata filters? (These are cheaper than semantic search.)
- Define relationships: If using Entity or Relation, define how entities/subjects/objects link.
- Prototype retrieval: Write retrieve() calls for each query pattern. Do filters return results in <\100ms?
- Measure costs: Estimate semantic vs. metadata retrieval breakdown. Aim for >70% metadata, <\10% semantic.
TypeScript Interface Examples
Ecommerce Customer Support
// Facts: preferences, settings
interface CustomerPreferenceFact {
type: "fact";
metadata: {
category: "preference" | "setting";
value: string;
strength: "strong" | "weak";
timestamp: string;
};
}
// Events: browsing, purchases, messages
interface CustomerActionEvent {
type: "event";
metadata: {
action: "browse" | "purchase" | "review" | "inquire";
entityId: string;
timestamp: string;
outcome?: "success" | "abandoned";
};
}
// Relations: who-bought-what, who-reviewed-what
interface PurchaseRelation {
type: "relation";
metadata: {
subject: string; // customer-id
predicate: "purchased" | "reviewed" | "refunded";
object: string; // product-id
strength: number; // confidence
timestamp: string;
};
}Conversational Agent
// Facts: user properties
interface UserPropertyFact {
type: "fact";
metadata: {
category: "name" | "role" | "location" | "preference";
value: string;
timestamp: string;
};
}
// Events: conversation turns
interface ConversationEvent {
type: "event";
metadata: {
action: "user_message" | "assistant_message" | "function_call";
role: "user" | "assistant";
turnNumber: number;
timestamp: string;
};
}
// Relations: user-mentions-entity, user-asks-about
interface MentionRelation {
type: "relation";
metadata: {
subject: string; // user-id
predicate: "mentions" | "asks_about" | "clarifies";
object: string; // topic, entity
turnNumber: number;
timestamp: string;
};
}Next Steps
- Deep dive: See Schema Design Patterns for advanced patterns (hierarchical types, polymorphism)
- Memory lifecycle: Review Longterm Memory for retention, eviction, and aging strategies
- Production: Check Cost Optimization for Agent Memory to optimize query cost by schema choice