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. Typed facts are
extracted automatically after the memory is stored. There are no manual
[[backlinks]] to maintain: if a memory says "Sarah works at
Acme", the graph gets a fact such as
(Sarah, works_at, Acme), whose subject and object are its
nodes, with Sarah typed as a person, and the fact points back 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
S["Sarah Lin"] -->|works_at| A["Acme Corp"]
A -->|priced_at| P["99 dollars per seat"]
S -->|reports_to| D["Dana Cho"]
style A fill:#6366f1,color:#fff,stroke:#6366f1
style S fill:#1e293b,color:#a5b4fc,stroke:#6366f1
style P fill:#1e293b,color:#94a3b8,stroke:#4b5563
style D fill:#1e293b,color:#94a3b8,stroke:#4b5563
What the graph contains
The graph is the typed facts. Each fact is a (subject, predicate,
object) triple whose subject and object are the nodes, with a subject
type (a person, an organization...) and aliases that join two names of
one entity. Every extracted fact points back to the memories that stated
it (source_memory_ids); a fact you write directly with
add_fact_triple has none. For example:
(Sarah Lin, works_at, Acme Corp),
(Acme Corp, priced_at, 99 dollars per seat). Every fact is
bi-temporal: it carries valid_from and
invalid_at, so contradictions supersede instead of
overwrite. See the example below.
Typed edges: predicates in 10 families
Facts carry the relation as the extractor phrased it
(predicate_raw) and a canonical form
(predicate). Each end user's vocabulary is learned: when the
model judges that a new phrase names a relation already known, the
phrase joins that relation, so a filter on predicate also
finds facts stored under phrases learned as the same relation. Each
relation is filed in one of 10 families, which you can filter on.
Examples of predicates you will see:
| 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 |
| other | catch-all for valid triples outside the 9 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: facts plus memories 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. Korely puts the
facts most relevant to the query (ranked by embedding similarity and
recency, at most 10) next to the memories that best match it, so the
block carries what is currently true as well as the verbatim source. 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 after a relative expression, as in "next month [June 2023]", was already resolved from the day that memory was written: use it as written and never recompute it from the current date. The date at the start of a memory, as in [2023-05-25], 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-09T00:00:00+00:00", "invalid_at": null, "invalidated_by": null, "source_memory_id": "mem_6e0f" }, { "id": "fct_8a3c", "subject": "Sarah Lin", "predicate": "works_at", "predicate_raw": "works_at", "object": "Acme Corp", "predicate_family": "work", "valid_from": "2026-06-09T00:00:00+00:00", "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-14T00:00:00+00:00", "invalid_at": "2026-06-09T00:00:00+00:00", "invalidated_by": null, "source_memory_id": "mem_18ba" } ], "total": 3}
Facts come back freshest confirmation first. 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, keeping it as history. A fact is live while its invalid_at is
null or still in the future. By default invalidated facts,
superseded or forgotten, are excluded; pass
include_invalidated=true to see the full chain, forgotten
facts included, when your agent needs to reason about how something
changed over time.
Note that predicate is the canonical form of the relation
and predicate_raw the phrase as extracted. 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), known_as_of (what the store knew then), 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-09T00:00:00+00:00 None# Sarah Lin works_at Acme Corp 2026-06-09T00:00:00+00:00 None## A fact is live when f.invalid_at is None or still in the future. f.predicate is the canonical# form; the phrase as extracted 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, a None or future invalid_at = 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 stored the memory and ran the write path:
it embeds the memory, and typed-fact extraction with
contradiction checking and bi-temporal validity runs right after the call returns. "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.) The same facts are
visible and correctable in the dashboard, and through the API in your
own 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 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.