The knowledge graph
The knowledge graph is an automatically maintained network of typed entities and relationships extracted from your agent's memories. For agent builders, it is the answer to the class of questions that vector search cannot handle: "Who does this person report to?", "What has changed about this customer's pricing?", or "What does the service this team owns depend on?", questions that require following connections, not ranking paragraphs. You write plain memories; Korely extracts the graph from them, with no external graph database, no manual tagging, and no schema to define. The graph is stored on our own EU-hosted infrastructure; the extraction reads each memory with the language model of the project's region, Google's Gemini API by default or gpt-oss-120b on Scaleway in Paris (see Security).
Every memory you add feeds a typed knowledge graph. Entities such as
people, organizations, products, places, concepts, and events are
extracted automatically on the write path. There are no manual
[[backlinks]] to maintain: if a memory mentions "Sarah from
Acme", the graph gets a person node, an
organization node, and edges connecting both to that
memory, without anyone lifting a finger. The graph builds itself.
This page explains what the graph contains, the two access patterns your agent uses to read it, a worked example you can replay, and why a traversable graph answers questions that similarity ranking structurally cannot.
flowchart LR
M1["M1<br/>Renewal call<br/>Acme Corp, Maria"] --> E1["Acme Corp"]
M1 --> E2["Maria"]
M2["M2<br/>Kickoff notes<br/>Acme Corp, PDF invoices"] --> E1
M2 --> E3["PDF invoices"]
M3["M3<br/>Contract draft<br/>Acme Corp, PDF invoices"] --> E1
M3 --> E3
style M1 fill:#6366f1,color:#fff,stroke:#6366f1
style M2 fill:#374151,color:#d1d5db,stroke:#4b5563
style M3 fill:#374151,color:#d1d5db,stroke:#4b5563
style E1 fill:#1e293b,color:#a5b4fc,stroke:#6366f1
style E2 fill:#1e293b,color:#94a3b8,stroke:#4b5563
style E3 fill:#1e293b,color:#94a3b8,stroke:#4b5563
What the graph contains
Two layers, built on top of each other:
- Entity layer. Typed nodes extracted from content (people, orgs, products, places, concepts, events) plus mention edges linking each entity to every item it appears in. This is what powers item-to-item traversal.
- Fact layer. (subject, predicate, object) triples
with typed predicates.
(Sarah Lin, works_at, Acme Corp),(Acme Corp, priced_at, 99 dollars per seat). Every fact is bi-temporal: it carriesvalid_fromandinvalid_at, so contradictions supersede instead of overwrite. See the example below.
Typed edges: a canonical vocabulary of predicates in 11 families
Facts use a canonical predicate vocabulary grouped into
11 families. Canonical means the extractor normalizes paraphrases: "is
employed by", "joined", and "works for" all land on
works_at, so your agent filters on one predicate instead of
guessing synonyms. A selection:
| Family | Example predicates |
|---|---|
| preferences | likes, dislikes, prefers |
| people | married_to, reports_to, knows |
| places | lives_in, located_in |
| work | works_at, works_on, manages |
| ownership | owns, uses, depends_on |
| health | allergic_to, practices |
| financial | priced_at, pays_for |
| events | attended, scheduled_for |
| identity | renamed_to, formerly, also_known_as |
| instructions | instructs, a standing instruction such as "reply in Italian". The extractor does not write it today: it holds the facts you write with this predicate. |
| other | catch-all for valid triples outside the 10 families above |
The families double as filters: the GET /v1/facts endpoint and
the SDK get_facts() method accept
predicate_family="financial" when you want everything
money-related about an entity without enumerating predicates.
Access pattern 1: fact-assembled recall via get_context
The primary way your agent reads the graph is
korely.get_context() (REST GET /v1/context). You
pass a query and a user_id; Korely assembles the active typed
facts plus the most relevant memories for that user into a single block,
pre-formatted and ready to drop into your model's prompt. This is the
differentiator: recall is driven by the typed-fact layer, not by ranking
raw chunks. The contradiction handling and bi-temporal validity described
below mean the facts you get back are the ones that are still true.
ctx = korely.get_context( query="What's the latest on the Acme renewal?", user_id="acme-account", agent_id="support-bot",)print(ctx.context)
_The facts below are a compact profile of the user; the memories are the verbatim source of truth. Ground your answer in this context. [...] do not abstain when it is present._
## Known facts- The user plans_to_send updated contract draft (since 2026-06-09) [planned, not done]- Sarah Lin works_at Acme Corp (since 2026-06-09)- Billing Service costs 99 dollars per seat (since 2026-06-09)- Sarah Lin reports_to Dana Cho (since 2026-06-09)
## Relevant memories_A time in square brackets [...] is the day it was said._- [2026-06-09] Call with Sarah Lin (Acme Corp). Renewal agreed at 99 dollars per seat for the Billing Service rollout. Sarah now reports to Dana Cho. Send the updated contract draft by Friday.
That is the block built from the single memory of the
worked example below.
It opens with a note for the model that reads it (shortened here), present
whenever the block has content and token_budget is at least
400. A fact whose subject is the end user reads "The user", and an
intention the text states as still to do is marked
[planned, not done].
The response is {context, tokens, sources, stable, volatile, stable_hash, degraded, degraded_parts}, where
sources mixes fct_ and mem_ ids so
you can trace every line back to a fact or a memory.
token_budget (default 800) caps the assembled size. The call
reuses stored vectors and the fact layer, so no generative model runs on
the read path, your agent's own model does the reasoning.
Access pattern 2: entity-centric facts
korely.get_facts(entity="X") (SDK) or
GET /v1/facts?entity=X (REST) queries the fact layer. The
entity filter matches X on either side of
the triple, subject or object. That detail matters: "Sarah Lin works_at
Acme Corp" (Acme as object) and "Acme Corp priced_at 99 dollars per
seat" (Acme as subject) both come back for
entity="Acme Corp". Your agent gets the full picture of an
entity in one call, not just the facts where it happens to be the
grammatical subject.
// GET /v1/facts?entity=Acme%20Corp&include_invalidated=true{ "facts": [ { "id": "fct_b91e", "subject": "Acme Corp", "predicate": "priced_at", "predicate_raw": "priced_at", "object": "99 dollars per seat", "predicate_family": "financial", "valid_from": "2026-06-09", "invalid_at": null, "invalidated_by": null, "source_memory_id": "mem_4a1c" }, { "id": "fct_07d2", "subject": "Acme Corp", "predicate": "priced_at", "predicate_raw": "priced_at", "object": "120 dollars per seat", "predicate_family": "financial", "valid_from": "2026-02-14", "invalid_at": "2026-06-09", "invalidated_by": "fct_b91e", "source_memory_id": "mem_18ba" }, { "id": "fct_8a3c", "subject": "Sarah Lin", "predicate": "works_at", "predicate_raw": "works_at", "object": "Acme Corp", "predicate_family": "work", "valid_from": "2026-02-14", "invalid_at": null, "invalidated_by": null, "source_memory_id": "mem_4a1c" } ], "total": 3}
The superseded fact (fct_07d2) is contradiction handling at
work. An earlier note priced Acme at 120 dollars per seat; a newer note
says 99. Two-stage contradiction detection set invalid_at on
the old fact and pointed invalidated_by at the replacement,
keeping it as history. A fact is live while its invalid_at is
null or still in the future. By default invalidated facts are
excluded, and facts the user chose to forget stay
excluded for good; pass include_invalidated=true to
see the full chain when your agent needs to reason about how something
changed over time.
Note that predicate is normalized: "is employed by", "joined",
and "works for" all return as works_at, with the original
verb preserved in predicate_raw. The response is a flat list
of facts plus a total, to get the same facts grouped by
predicate family with an as_of echo, use
get_profile() (GET /v1/profile) instead.
Other filters: subject (subject side only),
predicate, predicate_family, as_of
(point-in-time), and limit. Full parameter docs in the
API reference.
Calling get_facts from the SDK
The same query above, using the Python and Node SDKs. Both return a flat list of facts; invalidated facts are excluded by default.
from korely_memory import Korely
korely = Korely() # reads KORELY_API_KEY from env
# All facts where "Acme Corp" is subject or objectfacts = korely.get_facts( entity="Acme Corp", user_id="acme-account",)# facts is a flat list of Fact objects, attribute access:for f in facts: print(f.subject, f.predicate, f.object, f.valid_from, f.invalid_at)# Acme Corp priced_at 99 dollars per seat 2026-06-09 None# Sarah Lin works_at Acme Corp 2026-02-14 None## A fact is live when f.invalid_at is None. f.predicate is normalized;# the raw verb is f.predicate_raw.
# Narrow to financial facts onlyfinancial = korely.get_facts( entity="Acme Corp", predicate_family="financial", user_id="acme-account",)Reads are retrieval, not generation. No generative model composes output on the read path: graph traversal and fact lookup are pure SQL and index reads, and a facts query makes no model call at all. Your agent's own model does the reasoning on the results. This is also why read quotas are an order of magnitude more generous than write quotas.
Worked example: the graph builds itself
Here is the full loop via the SDK. The agent writes a memory from a sales call, then, a few seconds later, reads the typed facts back out of the graph that extraction built from it. Nobody creates a link or a triple at any point.
# Step 1, write
korely.add(
"Call with Sarah Lin (Acme Corp). Renewal agreed at "
"99 dollars per seat for the Billing Service rollout. "
"Sarah now reports to Dana Cho. Send the updated "
"contract draft by Friday.",
agent_id="support-bot",
user_id="acme-account",
)
# Step 2, read the typed facts. Graph built automatically, no triples by hand.
facts = korely.get_facts(
agent_id="support-bot",
user_id="acme-account",
)
for f in facts:
print(f.subject, f.predicate, f.object)
## Extracted facts (attribute access, invalid_at is None = live)
acme-account plans_to_send updated contract draft
Billing Service costs 99 dollars per seat
Sarah Lin reports_to Dana Cho
Sarah Lin works_at Acme Corp
One write, one read. The add() call ran the write path:
embeddings, entity extraction, and typed-fact extraction with
contradiction checking and bi-temporal validity. "Renewal agreed at 99 dollars per seat for the
Billing Service rollout" became
(Billing Service, costs, 99 dollars per seat), "Sarah now
reports to Dana Cho" became (Sarah Lin, reports_to, Dana Cho),
and the reminder to send the contract became a fact about the end user
marked as planned (tense planned). The subsequent
get_facts() call read those typed facts straight out of the
graph that extraction built, no manual linking, no tagging, no
maintenance. (Fact extraction is asynchronous, so on a brand-new memory
the facts populate within seconds of the write.) Korely pairs a typed
knowledge graph, bi-temporal facts, and an end-user memory app in one
product.
Why typed facts beat similarity ranking
Semantic search ranks chunks by vector similarity: embed the query, return the top-k most similar memories. That works for "find notes about Acme". It breaks on three question shapes that the typed fact layer handles structurally:
- Multi-hop questions. "What does the service Sarah
manages depend on?" requires following
managesand thendepends_on, two edges. No single chunk contains the answer, so no similarity score surfaces it. The fact layer holds both edges, so anentity-filtered facts query reaches the answer in two reads. - Entity disambiguation. "Apple" the employer and
"Apple" the vendor in your pricing notes are different relationships
around one node. Typed predicates keep
works_atandpriced_atseparate; embedding similarity smears them into one blob of Apple-flavored text. - "What do you know about X?" Answered structurally: every fact where X is subject or object, with validity dates. Compare that to "the 10 chunks most similar to the string X", which misses anything phrased differently and includes anything phrased similarly.
In practice agents combine both: korely.get_context() to
assemble the active facts plus relevant memories for a query in one call,
korely.search() (semantic vector search) when they want raw
candidate memories, and korely.get_facts() to read or filter
the typed graph directly. Recall finds the door; the typed facts tell you
what is still true behind it.
You and your users can see and fix all of this
The graph is not a black box your agent writes into. The dashboard's
Graph and Facts pages show every entity and fact, with Correct and Forget
on each fact, and the same actions are API calls, so your product can
give them to end users: correct_fact supersedes the old
fact, forget_fact closes it, delete_all erases
a user with an audit record. If extraction got something wrong, a human
fixes it and your agent reads the corrected fact on the next call.
Scope and limits
The graph is built from your content, automatically.
Your agent does not have to write triples; it writes memories, and
extraction turns them into entities and typed facts. (When it already
holds a structured triple, add_fact_triple writes it
directly.) The reliable way
to teach the graph something is to state it plainly:
korely.add() a sentence like "Acme renewed at 99 dollars per
seat" and the write path does the rest, as in the worked example
above.
-
GET /v1/facts(korely.get_facts()in the SDK) works on every plan, including the free Hobby tier, it needs only thememories:readscope and your monthly query quota. - Extraction quality depends on the source text. Clear declarative sentences extract reliably; a fact implied across three paragraphs of hedging may not. When precision matters, state it plainly in a memory.
Next: the API reference for exact parameters of every endpoint, or the quickstart if you have not wired a client yet.
See also
- Temporal facts & contradictions
, how bi-temporal validity (
valid_from/invalid_at) and two-stage contradiction detection work under the hood, and how to query facts at a point in time withas_of. - The memory model , the three-layer architecture (managed store, session memory, cross-session facts) that the graph sits on top of.
- API reference
, full parameter docs for
GET /v1/facts,GET /v1/context, and every other endpoint your agent calls.