Knowledge graph
The knowledge graph is the data substrate under everything. The four namespaces (memory, skills, tasks, user) live on top of it; so do every entity in every module (CRM, content, analytics).
If the agent ever cites a number, it came from here.
Shape
A typed graph in Postgres with three core tables:
| Table | Contents |
|---|---|
graph_nodes | Entities — contacts, deals, accounts, documents, conversations, memories, skills, tasks. Each has a kind, a JSON data payload, and an embedding. |
graph_edges | Typed relationships — belongs_to_account, replied_to, linked_to_deal, etc. Edges have their own data payload. |
graph_node_embeddings | Decoupled embedding storage; supports 1536-dim (cloud) and 768-dim (selfhost) in parallel columns so swapping embedding providers doesn't require a rewrite. |
Every module persists its entities as nodes. The agent doesn't query modules; it queries the graph. That's why agents can synthesize cross-module ("show me the deals that are stuck AND whose primary contact hasn't engaged with our content in 30 days") — the data is shape-compatible at the graph level.
Semantic search
The single most important RPC is graph_semantic_search. It accepts:
graph_semantic_search({
p_tenant_id: uuid, // always explicit — RLS contract
p_query_text: string, // for fuzzy fallback
p_query_embedding: vector, // primary signal
p_kinds?: string[], // filter by node kind
p_limit?: number, // top-N
p_min_score?: number, // floor
})
Returns ranked nodes with a score field combining:
- Vector cosine — embedding similarity (primary)
- pg_trgm — fuzzy text match (fallback when embedding dim mismatches, e.g. self-host on 768-dim)
- Recency boost — newer nodes get a small boost
- Kind weights — module-tunable
The agent calls this on every run. You can see what it pulled in the run drawer → Tool calls.
Embeddings
Cloud uses text-embedding-3-small (1536 dim). Self-host defaults
to nomic-embed-text from Ollama (768 dim). Both write to
graph_node_embeddings — into the column matching their dimension.
When graph_semantic_search is called, it picks the column matching
the runtime embedding provider. If the embedding column for the
runtime provider is empty (e.g. selfhost switched to OpenAI mid-
deployment), it falls back to pg_trgm fuzzy on query_text. Recall
degrades gracefully; the agent still gets results.
Citations
Every claim the agent makes is annotated with the graph nodes it came from. In the UI, hover any number, name, or summary — the citation drawer slides in showing the source rows. Click through to the entity for the full record.
The citation contract is enforced at the orchestrator's post-generation pass: if the model produces a number with no matching source in the assembled context, the post-processor flags it and the run is marked partial-confidence (still surfaces, with a visible warning).
Invalidation
When an entity changes — a deal moves stage, a contact gets edited
— the graph node's updated_at bumps and any dashboard card or
Today card that cited it gets invalidated. The card's regenerator
re-runs, the user sees fresh content. See
Dashboard surface for the invalidation
plumbing.
Triggers + backfill
Every module's table has a Postgres trigger that mirrors writes into the graph. The triggers are generated and shipped as part of each release, so adding a new module type auto-wires it. The backfill scripts handle the initial population when you upgrade past a schema change.
For operators: trigger code is shipped as migrations. You
shouldn't have to touch it day-to-day — ccd upgrade handles the
migration apply.
Multi-tenant safety
Every graph query is RLS-enforced. Even graph_semantic_search
takes p_tenant_id as an explicit parameter; the function does
its own tenant-isolation check before falling through to the
underlying tables (which also have RLS). Defense in depth.
The single exception is the cross-org benchmarks flow (cloud only) — it operates on aggregates only, with k-anonymity (k≥5) enforced. Raw nodes are never crossed. See Security model.
Performance
Common operations and their characteristics:
| Operation | Latency (typical) | Notes |
|---|---|---|
graph_semantic_search (kind-filtered, top 10) | 30–80 ms | IVFFlat index on embeddings, GIN on kind |
| Entity write + trigger fanout | 5–15 ms | Trigger is synchronous; keep payloads small |
| Card invalidation cascade | <100 ms | Async; runs in worker, not on the write path |
| Bulk import (1k contacts) | ~3 s | Embeddings batched; triggers honor batch context |
The graph is sized for typical tenants up to ~5M entity nodes. Beyond that, sharding by tenant becomes interesting; the schema supports it but no automation yet.
Read next
- Orchestrator — how an agent run actually uses the graph + context.
- Security model — RLS contracts and tenant isolation.