Concepts
Temporal facts
Every fact has two dates: when it became true and when it stopped. When a new fact contradicts an old one, Korely closes the old one: it sets an end date on it and keeps it.
Korely turns what your agents save
into facts. A fact has three parts: a
subject, a relation (called predicate in the API) and an
object, such as customer-4812 / subscribes_to / Business plan.
Terms
The Glossary defines the shared words. This page adds four.
- Judge: a language model inside Korely that checks each write for contradictions with older facts.
valid_from: the date a fact became true.invalid_at: the date it stopped; empty while it is true. The two make the valid time.known_as_of: a read parameter for what Korely knew on a date.
Quick example
You need Python 3.9 or later and pip install korely-memory
(checked with SDK 0.1.28). Get a key at
Get a free API key (Quickstart), then run
export KORELY_API_KEY=kor_live_....
The code writes two plans for the end user customer-4812. The customer is both the fact's subject and
the end user it belongs to (user_id); they can differ. It writes Starter from 1 May
2026, then Business from 1 June 2026, and reads both back.
from korely_memory import Korelykorely = Korely() # reads KORELY_API_KEY from the environmentkorely.add_fact_triple("customer-4812", "subscribes_to", "Starter plan", user_id="customer-4812", valid_from="2026-05-01")new = korely.add_fact_triple("customer-4812", "subscribes_to", "Business plan", user_id="customer-4812", valid_from="2026-06-01")print(new.invalidated)for f in korely.get_facts(user_id="customer-4812", include_invalidated=True): print(f.object, f.valid_from, f.invalid_at)You see this (ids are longer):
['fct_ad36...']Business plan 2026-06-01T00:00:00+00:00 NoneStarter plan 2026-05-01T00:00:00+00:00 2026-06-01T00:00:00+00:00- First line.
new.invalidatedholds the id of the Starter plan fact: the judge read the new plan as a replacement. - Second line. The Business plan is current:
invalid_atis empty. - Third line. The Starter plan closes on 2026-06-01, when the Business plan began.
Without include_invalidated=True, you get only the Business
plan. More filters: Get facts, the API reference.
If invalidated is empty, the judge saw no contradiction: close the old fact yourself with
forget_fact. If a write fails with 503
model_unavailable (the model could not be reached), nothing was
written: try again.
To run it again, use a new id in place of customer-4812.
How a new fact closes an old one
Korely checks whether a new fact contradicts an old one. If it finds a contradiction, it closes the old fact with a date instead of deleting it.
At each write, Korely takes two steps.
- Find possible conflicts. Korely looks at the
open facts of the same end user,
whichever agent wrote them. It takes:
- the same subject and relation: costs 40 and costs 50;
- the same subject and object with another relation: likes tea and dislikes tea;
- the same relation with another subject, only when you pass
user_id: "the user" and "Maria" can be one person, so a new fact can replace one written under another name. Withoutuser_idthis check is skipped. Since 1.0.12 it runs only between facts read from memories (see below).
Since 1.0.12, a fact you write yourself is compared only with facts about the same subject. For a fact written with POST /v1/facts (the Python SDK'sadd_fact_triple), or a correction that gives its subject (Correct a fact has the full rule), the judge sees only the earlier facts with the same subject, and the facts that link the same two things in the other direction. So a fact you write about one deploy no longer closes a fact about another deploy. "Maria" and the end user'suser_idcount as one subject only after you declare the name, with the sameagent_id. It works the other way too: a fact read from a memory is compared with a fact you wrote only when both have the same subject, or link the same two things in the other direction. Declaring a name later does not check again the facts already written: two values written before it both stay true, and a new value written later is checked against both. Two facts that the extraction reads from memories are still compared with every check above.
Since 1.0.13, a fact read from an agent's own memory is compared only with facts about the same subject. This is a memory written with anagent_idand nouser_id, such as a team's shared memory or an agent's own notes. The judge reads the earlier facts about the same subject, as Korely names it after the aliases of the agent that writes, and the facts that link the same two things in the other direction. Until 1.0.12 it also read the ten facts nearest in meaning, whatever their subject, and it sometimes closed facts that were both true ("the SDK repository has license MIT" closed by "the license is currently Apache 2.0"). A memory with auser_idis judged as before.
Since 1.0.19, the same rule covers every memory with nouser_id, with or without anagent_id(memory_core 6.9.0). Until 1.0.18 a memory written with no ids at all was still compared with the ten facts nearest in meaning, whatever their subject. Now the judge reads, for such a memory, the facts whose subject is the new fact's subject, whose object is its subject, or whose subject is its object ("Acme employs user", then "user works at Globex", still meet); a fact of another entity is no longer shown to the judge only because it is near in meaning, and two facts that only share an object do not meet. A memory with auser_idis judged exactly as before. Measured on 12 November 2026 on the memory of our own team of agents, written with no ids (43 memories, Gemini 3.1 Flash-Lite as writer and judge, three runs per version, every closure labelled by reading the two memories): 19 wrong closures of 41 with 1.0.18 and 12 of 30 with 1.0.19, none of them between two subjects (7 before). That is one memory, one model and small counts, not a rate for yours. What stays: the rules of the writer named "user" closing each other, which is one more reason a write now names an owner (Every write says whose memory it is).
What this costs. Two spellings of one thing ("Cloud" and "Korely Cloud") are two subjects, so a new value of one no longer closes the old value of the other, and a right closure of that kind is lost with the wrong ones. To make them one subject, declare one as an alias of the other withPOST /admin/aliases, in the namespace of the agent that writes, with itsagent_id. Without ids, the first person is stored as the subject "user": the alias to declare for a name is that name to "user", with nouser_idand noagent_id. An alias belongs to one agent: to make two spellings one subject for several agents, declare it for each of them. Every alias counts against the account's 1,000 declared names. Facts from different agents of one project are still compared when they name the subject the same way. A fact written with neither id and an agent's fact are compared with each other only when they are about the same subject, in whichever order they arrive, so a change told by one of them under another name leaves the other's value open. Facts written with no ids are compared among themselves as before. Anagent_idthat is empty or only whitespace is no agent: that memory is judged as one with no ids.
What is not measured. How many closures the new rule adds or loses with a real judge has not been measured. Facts already written, and those already closed, stay as they are. A fact closed wrongly stays readable withinclude_invalidated=true; write it again with POST /v1/facts to make it true again. - Judge. The judge reads the new fact next to these candidates and decides which ones it contradicts. Two plans for one subscription cannot both be true. "Likes coffee" and "likes tea" can.
The old fact ends when the new one begins (the moment of the write, if it has no date).
The judge can be wrong. It can miss a contradiction or
close a wrong fact. It may not read a change told in the past
tense, such as "Tom moved to Munich", as a replacement, so the earlier
fact can stay open. Check with get_facts and fix it with
correct_fact or forget_fact.
Where valid_from comes from
add() saves free text (a memory) and Korely extracts its facts.
add_fact_triple, used above, saves one fact already in its
three parts.
Korely uses the first that exists: the date you pass
(valid_from in add_fact_triple,
timestamp in add()); a date with a year in the
text ("Since June 2026" becomes 2026-06-01); the moment of the write.
When several memories carry the same event time, for example dates with no hour, Korely takes them in this order: the event time, then the moment its store received each one, then the id. So "moved to Paris" saved after "lives in Rome" is judged after it, and the newer fact stays open. The limit: memories saved in the same database transaction tie on the received moment, and the id decides. This holds since 1.0.0.
Add a memory, from text to facts, Write a fact, the API reference.
Read the past: as_of and known_as_of
as_of: what was true on a date, as Korely knows it today. It readsvalid_fromandinvalid_at.known_as_of: what Korely knew on a date. It readscreated_at, when Korely learned the fact, andclosed_known_at, when Korely learned that it had ended.
The answers differ when a change is told late. Korely knows
customer-4812 lives in Milan. On 5 October an agent learns of a move to
Turin on 1 September. It saves the present-tense note "customer-4812 lives in Turin"
with add(), passing the date of the move in the
timestamp parameter: timestamp="2026-09-01".
The Turin fact has created_at 5 October, when Korely learned
it. The Milan fact ends on 1 September (invalid_at), but its
closed_known_at is 5 October: Korely learned of the end only then.
as_of="2026-09-20"returns Turin: on 20 September the customer already lived there.known_as_of="2026-09-20"returns Milan: on 20 September Korely did not know of the move yet.
as_of on the Quick example
as_of returns the facts true on that date, each with the end
date it has today. A date without a time means midnight UTC.
Run this after the Quick example, in the same shell, for what was true on 15 May.
curl -G https://api.korely.ai/v1/facts \ -H "Authorization: Bearer $KORELY_API_KEY" \ --data-urlencode "user_id=customer-4812" \ --data-urlencode "as_of=2026-05-15"# 200 OK (some fields left out)# {"facts": [{"object": "Starter plan",# "valid_from": "2026-05-01T00:00:00+00:00",# "invalid_at": "2026-06-01T00:00:00+00:00"}], "total": 1}The Starter plan was true on 15 May, so it returns with its end date, 1 June.
Correct or forget a fact
-
correct_factis for a current fact with a wrong value: the new value counts from now on, the old fact is closed. -
forget_factcloses a fact with no replacement; its optionalatis the date it stopped being true. -
delete_all(user_id=...)erases every memory and fact of one end user, in the project of your API key. Database backups made before the call still hold the data.
Deleting the memory that closed a fact can open the fact again, as a new fact (Delete a memory says when). Since 1.0.14 Korely reads what closed a fact from what was written when it was closed, never from the dates: a fact you said is over by hand is not given back, and a fact that a memory said is over stays closed while that memory is live and still says it. Human in the loop: how end users read, correct and forget their facts.
Related
- The knowledge graph: how facts link names.
- The memory model: how memories and facts fit together.
- List events: whether a write has finished extracting its facts.