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 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
Three memories share entities. Entity extraction links M1, M2, and M3 through the shared Acme Corp node.

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 carries valid_from and invalid_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:

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
othercatch-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 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-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 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 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.

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(
  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 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.

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 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.