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 on our own EU-hosted infrastructure, with no external graph database, no manual tagging, and no schema to define.
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 9 families
Facts use a canonical predicate vocabulary grouped into
9 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 |
| other | catch-all for valid triples outside the 8 families |
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( "What's the latest on the Acme renewal?", user_id="acme-account", agent_id="support-bot",)print(ctx.context)
## Assembled context (typed facts + relevant memories)
## Known facts- Acme Corp priced_at 99 dollars per seat (since 2026-06-09)- Sarah Lin works_at Acme Corp (since 2026-02-14)- Sarah Lin reports_to Dana Cho (since 2026-06-09)
## Relevant memories- Renewal agreed at 99 dollars per seat for the Billing Service rollout.
The response is {context, tokens, sources}, 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 when its invalid_at is
null. 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 facts queries typically return in under 50 ms. 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 immediately 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(
entity="Acme Corp",
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 Corp priced_at 99 dollars per seat
Sarah Lin works_at Acme Corp
Sarah Lin reports_to Dana Cho
One write, one read. The add() call ran the write path:
document and chunk embeddings, entity extraction on our own
infrastructure, and typed-fact extraction with contradiction checking
and bi-temporal validity. "Renewal agreed at 99 dollars per seat" became
the priced_at fact, and "Sarah now reports to Dana Cho" became
(Sarah Lin, reports_to, Dana Cho). 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.
The end user can see and edit all of this
The graph is not a black box your agent writes into. The memory owner sees the same facts in the Korely desktop and web app. A Memory Panel lists every fact with edit and forget actions, and an Entity Profile drawer shows everything attached to one entity. Edits supersede the old fact; forgetting erases it, with an audit cascade. If extraction got something wrong, the human fixes it and your agent reads the corrected fact on the next call. Korely is the only memory layer where end users can see, correct, and delete their own memory in a real app.
Scope and limits
The graph is built from your content, automatically.
Your agent does not write triples directly; it writes memories, and
extraction turns them into entities and typed facts. 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.