Context & facts
Write a fact
Write a typed (subject, predicate, object) triple directly. Korely skips LLM extraction but still runs the contradiction check when the install has a model key, so a conflicting older fact is superseded, not duplicated.
/v1/facts
SDK: korely.add_fact_triple(subject, predicate, object, ...). Use
this when you already have a structured fact and want to skip extraction
(model_used is recorded as direct). With a model
key configured, the two-stage contradiction check still runs, which is why
the response carries
invalidated, the ids of any earlier facts this write
closed. Only a write answers invalidated;
a later read shows the same link as
invalidated_by on the closed fact.
Authentication
HTTP header, required: Authorization: Bearer kor_live_.... The key must carry the memories:write scope.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
subject | string | Required | Subject side of the triple. 1-200 characters; stripped before write. |
predicate | string | Required | Raw predicate; canonicalized server-side. 1-200 characters; stripped before write. |
object | string | Required | Object side of the triple. 1-500 characters; stripped before write. |
user_id | string | One of user_id and agent_id | The end user this fact is about. A fact with neither user_id nor agent_id is refused with 422 identity_required (why). 1 to 255 characters, no /, no control characters, not . or ... Pass it: with a user_id the check also compares the fact with facts written under another name of the same person, once that name is declared (see Notes). |
agent_id | string | One of user_id and agent_id | Your application's namespace (agent_id): the agent writing. Same rule as user_id. A name new to the project counts against your plan's agent cap on the Cloud. |
run_id | string | Optional | One session or run. Max 255 chars. |
subject_type | string | Optional | What the subject is: any type of up to 20 characters, stored as sent on a new fact (an empty one is stored as unknown, which is also the default). If the fact is already true, the write keeps the type the fact was first stored with and answers that fact's id, so a self sent then is not stored. self (lower case, matched exactly) marks a fact about the end user: Get profile lists it first. |
object_is_literal | boolean | Optional | Whether the object is a literal value rather than an entity. Default false. |
confidence | number | Optional | Confidence score between 0.0 and 1.0. Default 0.9. |
valid_from | string | Optional | ISO 8601 date or datetime the fact becomes valid. Sets the valid time start, valid_from; use it when backfilling so the timeline stays correct. When omitted, the fact is valid from the moment of the write. |
tense | string | Optional | current, past or planned. Default current. A past fact closes the open fact it restates; with no open twin it is stored as history, already closed. You say the tense; Korely does not infer it from the verb. A past fact with no valid_from begins and ends when it was written, an empty interval, so no as_of read returns it as true. With a valid_from it is true from that date until it was told (both since 1.0.0). |
Any other field is refused with 422 invalid_request: field names are snake_case.
Example request
curl -X POST https://api.korely.ai/v1/facts \ -H "Authorization: Bearer kor_live_..." \ -H "Content-Type: application/json" \ -d '{ "subject": "customer-giulia-4812", "predicate": "prefers", "object": "dark mode", "user_id": "customer-giulia-4812", "agent_id": "support-bot", "subject_type": "person", "object_is_literal": true, "confidence": 0.95 }'Response
201 Created. The newly written fact, plus the ids of any
earlier facts it superseded.
{ "id": "fct_a1", "subject": "customer-giulia-4812", "predicate": "prefers", "object": "dark mode", "predicate_family": "preferences", "valid_from": "2026-06-17T00:00:00+00:00", "invalidated": ["fct_a0"], "tense": "current", "invalid_at": null, "observation_count": 1, "user_id": "customer-giulia-4812", "agent_id": "support-bot"}| Field | Type | Description |
|---|---|---|
id | string | Public id of the newly written fact, e.g. fct_a1 (fct_ prefix). |
subject | string | Stored subject label. |
predicate | string | The predicate as stored: your phrase in snake_case form (prefers, works_at). A direct write keeps the name you chose, or the relation that name was already learned as in this vocabulary; the model never merges it into another relation. |
object | string | Stored object label. |
predicate_family | string · null | Predicate family derived from the canonical predicate. |
valid_from | string · null | ISO 8601 timestamp the fact is valid from, or null. |
invalidated | array<string> | Public ids of pre-existing facts this write superseded via the contradiction check. Empty list if none. |
invalid_at | string · null | ISO 8601 timestamp the fact stopped being true. Set when you write a past fact; null otherwise. |
tense | string | The tense the fact was stored with: current, past or planned. |
observation_count | integer | How many times this fact has been stated. Writing a triple that is already open confirms it and raises the count instead of adding a duplicate. |
user_id | string or null | The end user the fact belongs to, as in GET /v1/facts. |
agent_id | string or null | The agent namespace it was written under, as in GET /v1/facts. |
Contradictions resolve on write. When the install has a
model key, as the Cloud does, a current fact goes through
the two-stage contradiction check even on a direct write: open facts of
the same subject that could conflict are gathered, and a model judges
whether the new fact ends them, reading whether the relation holds one
value at a time. A fact it ends is closed, not deleted, and its id
appears in invalidated. Superseded facts stay queryable via
as_of. A Self-hosted install with no model key runs no judge
and closes nothing this way; a past fact still closes the
open fact it restates. The judge can be wrong: it can miss a
contradiction or close the wrong fact. Read the facts back, and close
one yourself with Close a fact.
Errors
| Status | Code | Cause |
|---|---|---|
401 | invalid_key | Missing or invalid kor_live_ key in the Authorization header. |
403 | forbidden | The API key lacks the memories:write scope. |
413 | payload_too_large | The body is larger than 2,097,152 bytes. Refused before it is read; nothing is written or counted. On the Cloud a body over 1 MB gets a bare 413 first, without this code. |
429 | agent_cap_exceeded | Writing with a new agent_id would exceed your plan's named agents (the body adds limit and used). Reuse an existing agent_id, delete one with DELETE /v1/agents/{agent_id}, or upgrade. Checked before the write quota, so a capped write doesn't burn a quota count. |
429 | rate_limit_exceeded | Per-plan rate limit exceeded. Retry after the Retry-After header. |
429 | quota_exceeded | The month's writes are used up, 10% grace included; nothing is written. The body adds limit, used and resets_at (00:00 UTC on the 1st) to code and message. Top up the month on a paid plan, or upgrade. |
422 | identity_required | The fact names neither user_id nor agent_id (or only ids of spaces, invisible characters or combining marks). Nothing is written. A key confined to one end user needs neither: the fact goes to that end user. |
422 | invalid_request | Body validation failed: subject/predicate/object missing or over their length limits, confidence outside [0, 1], a non-ISO valid_from, a tense other than the three values, a user_id or agent_id breaking the id rule, or an unknown field. |
503 | model_unavailable | The model that resolves the relation against your learned vocabulary, or the contradiction check, gave no usable answer. Nothing was written; retry the request. |
503 | service_paused | The service itself does not call the models now (for example a spending limit, or a problem at the model provider). Carries Retry-After: 3600: a retry within the hour cannot succeed. Nothing was written. |
503 | unavailable | The key could not be checked because the database did not answer, or the request waited past one of the store's deadlines. Nothing was written; retry after the seconds in Retry-After. |
408 | request_timeout | The body did not arrive whole within 30 seconds. Nothing was written; send it again. |
Notes
- Skips extraction, not the check. A direct write bypasses LLM extraction (
model_usedisdirect) but still runs the two-stage contradiction check when the install has a model key, that's why the response carriesinvalidated. - Compared only with the same subject (since 1.0.12). The contradiction check shows the judge two kinds of earlier facts only: facts with the same subject, and facts that link the same two things in the other direction ("Globex dropped Salesforce" for "Salesforce serves_as_crm_for Globex"). Up to 1.0.11 it also showed facts with other subjects that were near in meaning, so "deploy Cloud 1.0.8 happened on 5 November" could close "deploy Cloud 1.0.7 happened on 2 November". Two subjects are the same when they match ignoring upper and lower case, or when an alias joins them in the same
agent_id: a declared name, an alias made in the dashboard or with an admin key, or a rename Korely read in a memory (such as "Acme was renamed Globex").Annaandannaare one subject,Anna RossiandAnnaare two. A new value for the same subject can still close the old one. The rule works both ways: 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. The cost: the judge may now miss closures that were right. For example, a fact aboutMariaand a fact read from a memory of the end useru-42are compared only after you declareMariaas a name ofu-42. How many right closures are lost is not measured with a real model.invalidatedcan list fewer ids than before. - Idempotent by triple. If an active fact already exists with the same subject, canonical predicate, and object for the same end user and
agent_id, Korely reconfirms it instead of writing a duplicate: the answer is still201, with the sameid,invalidated: []andobservation_countone higher. - It counts as one write. A fact written directly uses one write of your monthly quota, because a model reads it. A call that writes nothing, such as a
503, uses none. - Values are stripped.
subject,predicate, andobjectare trimmed before write. - Agent cap before quota. When you pass a new
agent_id, the agent-cap check runs before the write-quota check, so a cap failure never consumes a quota slot. - Webhooks and audit. The write commits, sends a
fact.invalidatedwebhook for each fact it closed, and is logged as afacts:createrequest.
Related
- Get facts, read the typed facts back.
- Get context, the assembled recall block, facts included.
- SDK reference, every method, typed.