Korely

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
Three facts that share a subject or an object. Each fact is an edge between two nodes, and each points back to the memories that stated it.

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:

FamilyExample predicates
preferenceslikes, dislikes, prefers
peoplemarried_to, reports_to, knows
placeslives_in, located_in
workworks_at, works_on, manages
ownershipowns, uses, depends_on
healthallergic_to, practices
financialpriced_at, pays_for
eventsattended, scheduled_for
identityrenamed_to, formerly, also_known_as
othercatch-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 object
facts = 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 only
financial = 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.

korely sdk agent session
# 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 manages and then depends_on, two edges. No single chunk contains the answer, so no similarity score surfaces it. The fact layer holds both edges, so an entity-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_at and priced_at separate; 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 the memories:read scope 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 with as_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.