ChanlChanl
Knowledge & Memory

Your agent doesn't need more memory. It needs a graph.

Session memory forgets. RAG retrieves but doesn't reason. Context graphs give your CX agent a persistent, queryable model of every customer, issue, and relationship it's ever encountered -- at a fraction of the token cost.

DGDean GroverCo-founderFollow
July 6, 2026
16 min read
Graph visualization showing customer entity nodes connected to order, issue, and conversation nodes with relationship labels and timestamps

Your CX agent forgets everything the moment a session ends.

A customer calls to follow up on a refund they requested last week. Your agent has no idea. It asks for the order number again. The customer sighs. It asks for their email address. The customer sighs again. This isn't just an annoying user experience -- it's an agent architecture problem.

The instinct is to add more memory. Give the agent access to previous conversation transcripts. Feed it the last N sessions as context. Use RAG to retrieve relevant history. These approaches all share the same flaw: they give the agent more text to read without giving it a model of what it already knows. The agent still has to extract the relevant facts from unstructured text on every turn, under token pressure, at inference time.

Context graphs flip the model. Instead of "let the agent re-derive facts from raw history," you build a persistent, structured representation of what you know about each customer, update it after every conversation, and inject the relevant subgraph into each new session. The agent starts every call knowing who it's talking to.

What a context graph actually is

A context graph is a typed property graph where nodes represent entities and edges represent relationships between them, both with timestamps. This is different from vector RAG, which stores text chunks and retrieves by similarity. It's different from flat key-value memory, which stores facts as isolated properties. A context graph understands the connections between facts.

For a CX application, the core entity types are:

  • Customer: name, contact info, account status, preferences
  • Order: ID, product, date, status, value
  • Product: name, SKU, category, known issues
  • Issue: type, severity, created date, resolution
  • Conversation: date, channel, agent, summary, sentiment
  • Agent: name, team, specialization

The relationships between them model what actually happened:

  • Customer PLACED Order
  • Order CONTAINS Product
  • Order HAS_ISSUE Issue
  • Issue DISCUSSED_IN Conversation
  • Issue RESOLVED_AS Resolution
  • Customer CONTACTED_VIA Channel
  • Issue ESCALATED_TO Agent

Each node and edge carries timestamps. Each edge can carry a weight reflecting how recent or confident the relationship is. A customer who mentioned a preference three months ago in passing gets a lower confidence weight than one who confirmed it last week.

PLACED PLACED HAS_ISSUE HAS_ISSUE DISCUSSED_IN DISCUSSED_IN ESCALATED_TO FOLLOWED_UP CustomerMeridian Corp Order #77412026-06-10 Order #88122026-07-01 Issue: Late deliverySeverity: medium Issue: Wrong itemSeverity: high Conversation2026-06-15 Conversation2026-07-03 Agent: Sarah KBilling team Conversation2026-07-06
CX context graph: Customer node connected to Orders, Issues, and Conversations with typed relationships

When your agent starts a new conversation with this customer, it doesn't need to read all three conversation transcripts. It queries the graph and gets back: two recent orders, two open issues, one escalated case, and the fact that today's call is a follow-up on issue #8812. That's 400 tokens of structured context instead of 6,000 tokens of raw transcripts.

How this differs from what you're probably doing

Context graphs outperform session memory because they persist across sessions, and they outperform RAG because they model relationships rather than finding similar text. Session memory forgets on disconnect. RAG retrieves relevant chunks but can't answer "what has this customer's history been?" Context graphs answer that question in a single traversal.

The most common memory pattern for CX agents is session memory: a dictionary that accumulates facts during a conversation and gets cleared at the end. The next call starts fresh. You've probably seen the alternative discussed as "conversation history injection" -- stuffing the last N transcripts into the context window. Both approaches have the same problem: every session, the agent re-reads everything.

RAG is better, but still incomplete. GraphRAG and vector retrieval give the agent relevant text chunks from previous conversations, which is much better than nothing. But similarity search finds related text -- it doesn't reason about relationships. "This customer has had two shipping issues in four weeks" is a relationship fact that emerges from querying the graph, not from finding documents similar to "shipping problem."

The key insight from Gartner's 2026 research is that 65% of enterprise AI agent failures in production were traced to context drift or memory loss during multi-step reasoning tasks -- not model capability limitations. The model was capable. It just didn't know enough about the situation to act correctly.

Context graphs address that by shifting the memory problem from inference time to write time. The agent doesn't figure out what it knows by re-reading history. The fact extraction pipeline already did that work after each conversation, and the graph already encodes the conclusions.

As we covered in session vs. long-term memory, the challenge isn't storage -- it's retrieval and relevance. Context graphs solve retrieval because a graph traversal from a known entity is deterministic and fast. A 3-hop traversal from a customer node to their recent issues and conversations takes under 10ms in Neo4j at scale. You know exactly what you got and can explain it.

Building the graph schema

Start with Customer, Order, and Issue nodes with PLACED and HAS_ISSUE edges. That's enough to answer most CX queries. Add Conversation nodes once your fact extraction pipeline is running. Add Agent and Product nodes when you're tracking escalation patterns and product-specific issues. Don't design the full schema upfront -- let real queries tell you what to add next.

Before you write code, define a schema that reflects your actual CX workflow. Here's a starting point for a SaaS support context:

cx-context-schema.ts·typescript
// Node types
interface CustomerNode {
  id: string;           // Your internal customer ID
  name: string;
  email: string;
  tier: "free" | "pro" | "enterprise";
  createdAt: Date;
  lastSeenAt: Date;
  preferences: Record<string, string>;  // channel, language, etc.
}
 
interface OrderNode {
  id: string;
  customerId: string;   // Denormalized for quick lookups
  productName: string;
  status: "pending" | "active" | "cancelled" | "refunded";
  value: number;
  createdAt: Date;
}
 
interface IssueNode {
  id: string;
  type: string;         // "billing", "shipping", "product-defect", etc.
  severity: "low" | "medium" | "high" | "critical";
  status: "open" | "in-progress" | "resolved" | "escalated";
  createdAt: Date;
  resolvedAt?: Date;
  resolution?: string;
}
 
interface ConversationNode {
  id: string;
  channel: "voice" | "chat" | "email" | "messaging";
  agentId?: string;
  sentiment: "positive" | "neutral" | "negative";
  summary: string;      // LLM-generated, 2-3 sentences
  createdAt: Date;
  durationSeconds?: number;
}
 
// Edge types
interface PlacedEdge {
  type: "PLACED";
  weight: number;       // 1.0 for recent, decays toward 0
  createdAt: Date;
}
 
interface HasIssueEdge {
  type: "HAS_ISSUE";
  createdAt: Date;
  confirmedAt: Date;    // When issue was formally linked to order
}

The schema grows with your use cases. Don't over-design it upfront. Start with Customer, Order, and Issue nodes and the PLACED and HAS_ISSUE edges. Add Conversation nodes once you have a fact extraction pipeline running. Add Agent nodes once you're tracking escalation patterns.

Writing to the graph after each conversation

After each session ends, run a fact extraction pass over the transcript with an LLM. It identifies entity mentions (customer, order, product, issue type) and relationships (expressed intent, resolution accepted, escalation required) and writes them as nodes and edges with the conversation ID as provenance. Run this asynchronously so it doesn't block the customer experience.

Facts don't write themselves. You need a pipeline that reads the conversation transcript after each session and extracts entities and relationships into the graph.

fact-extractor.ts·typescript
import Anthropic from "@anthropic-ai/sdk";
import neo4j from "neo4j-driver";
 
const anthropic = new Anthropic();
const driver = neo4j.driver(process.env.NEO4J_URI!, neo4j.auth.basic(...));
 
interface ExtractedFact {
  entityType: "Customer" | "Order" | "Issue" | "Product";
  entityId: string;
  properties: Record<string, unknown>;
  relationships: Array<{
    type: string;
    targetEntityType: string;
    targetEntityId: string;
    properties?: Record<string, unknown>;
  }>;
}
 
async function extractFacts(
  transcript: string,
  customerId: string,
  conversationId: string
): Promise<ExtractedFact[]> {
  const response = await anthropic.messages.create({
    model: "claude-sonnet-5",
    max_tokens: 1024,
    messages: [{
      role: "user",
      content: `Extract structured facts from this CX conversation transcript.
Return JSON with entities and relationships discovered.
 
Customer ID: ${customerId}
Conversation ID: ${conversationId}
 
Transcript:
${transcript}
 
Return format: { facts: ExtractedFact[] }
Focus on: order numbers, issue types, product mentions, resolutions, escalations.
Only extract facts explicitly stated, not inferred.`
    }]
  });
 
  const content = response.content[0];
  if (content.type !== "text") return [];
 
  try {
    const parsed = JSON.parse(content.text);
    return parsed.facts || [];
  } catch {
    return [];
  }
}
 
async function writeFactsToGraph(facts: ExtractedFact[], conversationId: string) {
  const session = driver.session();
  try {
    for (const fact of facts) {
      await session.run(`
        MERGE (n:${fact.entityType} { id: $id })
        SET n += $properties, n.updatedAt = datetime()
        WITH n
        MERGE (conv:Conversation { id: $conversationId })
        MERGE (n)-[:MENTIONED_IN { createdAt: datetime() }]->(conv)
      `, {
        id: fact.entityId,
        properties: fact.properties,
        conversationId,
      });
 
      for (const rel of fact.relationships) {
        await session.run(`
          MATCH (source:${fact.entityType} { id: $sourceId })
          MERGE (target:${rel.targetEntityType} { id: $targetId })
          MERGE (source)-[r:${rel.type}]->(target)
          SET r += $relProperties, r.updatedAt = datetime()
        `, {
          sourceId: fact.entityId,
          targetId: rel.targetEntityId,
          relProperties: { ...rel.properties, weight: 1.0 }
        });
      }
    }
  } finally {
    await session.close();
  }
}

Run this pipeline asynchronously after each conversation ends. It doesn't block the customer experience -- it runs in the background, updating the graph while the agent is handling the next call.

Querying context at the start of each turn

At the start of each conversation, traverse the graph from the customer's node to their active orders, open issues, and recent conversations -- typically 2-3 hops. Serialize the result into a compact structured block injected into the system prompt before the first user message. The agent starts every call knowing who it's talking to and what's unresolved.

When a new conversation starts, query the graph to build the agent's initial context:

context-injection.ts·typescript
async function buildCustomerContext(customerId: string): Promise<string> {
  const session = driver.session();
  try {
    // 2-hop traversal from customer to orders, issues, and recent conversations
    const result = await session.run(`
      MATCH (c:Customer { id: $customerId })
      OPTIONAL MATCH (c)-[:PLACED]->(o:Order)
        WHERE o.status IN ['active', 'pending']
      OPTIONAL MATCH (o)-[:HAS_ISSUE]->(i:Issue)
        WHERE i.status IN ['open', 'in-progress', 'escalated']
      OPTIONAL MATCH (i)-[:DISCUSSED_IN]->(conv:Conversation)
        WHERE conv.createdAt > datetime() - duration('P30D')
      RETURN c, collect(DISTINCT o) as orders,
             collect(DISTINCT i) as issues,
             collect(DISTINCT conv) as recentConvs
      ORDER BY conv.createdAt DESC
      LIMIT 5
    `, { customerId });
 
    if (result.records.length === 0) {
      return "No prior context for this customer.";
    }
 
    const record = result.records[0];
    const customer = record.get("c").properties;
    const orders = record.get("orders").map((o: any) => o.properties);
    const issues = record.get("issues").map((i: any) => i.properties);
    const convs = record.get("recentConvs").map((c: any) => c.properties);
 
    // Serialize to a compact structured block
    return `
CUSTOMER CONTEXT (${new Date().toISOString()})
Customer: ${customer.name} | Tier: ${customer.tier} | Since: ${customer.createdAt}
 
Active orders: ${orders.length === 0 ? "None" : orders.map(o =>
  `${o.id} (${o.productName}, status: ${o.status})`).join("; ")}
 
Open issues: ${issues.length === 0 ? "None" : issues.map(i =>
  `${i.type} on ${i.createdAt?.toString().split("T")[0]} (${i.status})`).join("; ")}
 
Recent interactions: ${convs.length === 0 ? "None" : convs.map(c =>
  `${c.channel} on ${c.createdAt?.toString().split("T")[0]} -- ${c.summary}`).join(" | ")}
    `.trim();
  } finally {
    await session.close();
  }
}

This produces a context block the agent receives before any user input. A customer with two open issues and a recent escalation gives the agent something to work from. The agent can greet them by name, reference the escalated case, and not waste the first two turns asking questions it should already know.

Customer service representative

Customer Memory

4 memories recalled

Sarah Chen
Premium
Last call
2 days ago
Prefers
Email follow-up
Session Memory

“Discussed upgrading to Business plan. Budget approved at $50k. Follow up next Tuesday.”

85% relevance

Managing staleness and decay

Apply time-decay weights to edges using an exponential half-life that you tune by relationship type. Issue relationships decay slowly -- a billing dispute is worth knowing for a year. Preference relationships decay faster. One-time contact channel relationships may be worth dropping after 90 days. The decay function runs at query time, filtering out low-weight edges before they reach the agent.

Facts change. A shipping address from two years ago might be wrong. A preference noted in a single conversation might not represent a stable pattern. A context graph that never forgets is as bad as one that forgets too quickly.

Apply time-decay weights to edges. A simple exponential decay function:

decay.ts·typescript
function edgeWeight(createdAt: Date, halfLifeDays: number = 60): number {
  const ageMs = Date.now() - createdAt.getTime();
  const ageDays = ageMs / (1000 * 60 * 60 * 24);
  // Exponential decay: weight = 0.5^(age/halfLife)
  return Math.pow(0.5, ageDays / halfLifeDays);
}

Tune the half-life by relationship type. Issue relationships decay slowly -- if a customer had a billing dispute, that's worth knowing for a year. Preference relationships decay faster -- tastes change. One-time contact channel relationships might be worth dropping after 90 days without re-confirmation.

Also consider explicit expiry. Add an expiresAt property to edges where expiry makes business sense: a promotional discount relationship, a temporary escalation path, a time-limited override. Your query filters out expired edges automatically.

For CX deployments, review your staleness strategy every quarter. The right decay parameters depend on how often your product changes (which makes product-related preferences go stale faster) and how frequently customers interact (high-frequency customers' graphs stay fresh naturally; low-frequency customers' graphs need more aggressive decay).

Track graph health in your monitoring dashboard. Watch for: average node staleness, percentage of edges below your minimum weight threshold, and fact extraction failure rate. A graph that stops receiving new facts quickly becomes a liability rather than an asset.

When to use context graphs vs. simpler approaches

Use context graphs when your agents handle repeat customers across multiple sessions and the history of those interactions changes what the agent should do. Skip them for single-transaction agents where history genuinely doesn't matter. The infrastructure cost -- a graph database, a fact extraction pipeline, a schema to maintain -- pays off when relationship traversal is the difference between a good agent response and a frustrating one.

Context graphs add real operational complexity. A graph database to run, a fact extraction pipeline to maintain, a schema to evolve as your product changes.

For agents that handle one-off transactions where history doesn't matter -- a one-time password reset, a single product lookup -- plain session memory is fine. Don't build infrastructure for relationships that don't exist.

For agents handling repeat customers with ongoing issues across multiple conversations -- account management, technical support, subscription services -- context graphs pay off quickly. The token savings alone (70% reduction vs. conversation history injection) often justify the infrastructure cost within the first month of production traffic.

The middle ground is Chanl's memory feature, which manages entity extraction and graph writes as part of the agent's conversation loop. You define the entity schema and relationships you care about; the extraction and storage happens automatically. For agents already on Chanl, this removes the pipeline-building work:

chanl-memory.ts·typescript
import { Chanl } from "@chanl/sdk";
 
const chanl = new Chanl({ apiKey: process.env.CHANL_API_KEY });
 
// Fetch context graph for a customer before the conversation starts
const context = await chanl.memory.search({
  entityType: "Customer",
  entityId: customerId,
  depth: 2,             // How many hops to traverse
  maxFacts: 20,         // Cap to control token budget
  includeRelationships: ["PLACED", "HAS_ISSUE", "CONTACTED_VIA"]
});
 
// Serialize for prompt injection
const contextBlock = chanl.memory.toContextBlock(context);

After the conversation, the analytics pipeline automatically runs fact extraction and writes back to the graph. The analytics feature uses the same graph to power conversation intelligence reports -- patterns you couldn't detect from individual sessions become visible across the full customer relationship history.

The shift from retrieval to reasoning

Context graphs don't just improve recall. They change what the agent can reason about. An agent with a structured model of the customer's history can detect patterns, infer severity, and propose solutions before the customer finishes explaining. An agent without one is re-deriving the same facts from documents on every turn.

The difference isn't just recall. It's reasoning quality.

An agent that knows a customer has had two shipping issues in four weeks doesn't just avoid asking redundant questions. It can reason about the pattern: "This looks like a systemic issue with their region's carrier, not a one-off." It can proactively route to the right team. It can offer appropriate compensation without being prompted.

That reasoning depends on the agent having a model of the situation, not just a pile of documents to search. Context graphs provide that model.

The complexity is real -- schema design, extraction pipelines, staleness management. But the payoff is an agent that behaves like it actually knows the customer, because it does.

The customer who called about a refund last week and has to explain the situation again from scratch? That's a context graph problem, not a model problem. With a graph in place, the agent already knows about the refund. It knows it's still pending. It knows this is the follow-up call. No sighs required.

Give your agents persistent customer memory

Chanl's memory feature automatically extracts facts from every conversation and builds a context graph you can query at the start of each new session. Your agent knows the customer's history before the first word.

Explore Memory
DG

Co-founder

Building the platform for AI agents at Chanl — tools, testing, and observability for customer experience.

El briefing de Signal

Un email por semana. Cómo los equipos líderes de CS, ingresos e IA están convirtiendo conversaciones en decisiones. Benchmarks, playbooks y lo que funciona en producción.

500+ líderes de CS e ingresos suscritos

Frequently Asked Questions