Core operations
Add a memory
Store content and run the full write pipeline, embeddings, entity graph, typed facts, and contradiction detection, in a single call.
When you call add(), Korely stores the raw content, generates
a document embedding, and runs typed bi-temporal fact extraction with a
two-stage contradiction check; the subjects and objects of those facts are
the entities of the graph.
Any fact that conflicts with an existing one is marked superseded rather than
deleted, so the full history stays queryable. Fact extraction runs
asynchronously: the response may already carry the facts from this write, or
the facts array may arrive empty and populate moments later. Read
them back with get_context or
get_facts() once the pipeline settles.
Call add() after any turn where the user reveals something worth
remembering: a preference, a decision, a fact about themselves or their
environment. Through the SDK you can also hand it the full message list from a
chat session, it is joined into one block and the pipeline extracts what is
worth keeping from the whole conversation.
flowchart LR
A([add content]) --> B[Store memory]
B --> C[Embed memory]
C --> D[Entity graph]
D --> E[Fact extraction]
E --> F{Contradiction?}
F -- yes --> G[Supersede old fact]
F -- no --> H[Record new fact]
G --> I([Return memory + facts])
H --> I Request
Endpoint: POST /v1/memories. SDK: korely.add(content, *, user_id=None, agent_id=None, run_id=None, metadata=None, timestamp=None).
| Param | Type | Notes |
|---|---|---|
content | string | Required. Plain text or Markdown, the REST body takes a string (max 16,000 chars). The Python and Node SDKs additionally accept a list of chat messages ([{role, content}]) and join them into one text block before sending. The pipeline extracts facts from the resulting text. |
user_id | string | Optional but recommended. Your end user's identifier (e.g. "customer-4812"). Scopes the memory to one person your agent serves. End users are unlimited on every plan. 1 to 255 characters, no /, no control characters, not . or ..; omit it for the shared namespace (an empty string is refused). |
agent_id | string | Optional. Namespaces the memory to one of your apps; same id rule as user_id. Omit it for the default namespace (agent_id null), which never counts toward the cap. Each distinct agent_id counts toward your plan's agent limit; a new one past the limit returns 403 agent_cap_exceeded. |
run_id | string | Optional, max 255 characters. Sub-scope for one agent run or chat session. Useful when you want to isolate a single conversation from the user's broader memory. |
metadata | object | Optional. Arbitrary key-value pairs returned verbatim on read; not interpreted by the pipeline. Use it for source tags, version labels, or any application data you want to round-trip. At most 8,192 bytes once serialized. |
timestamp | string | Optional. ISO 8601 date or datetime when the events happened, for backfills and migrations. The extracted facts take it as valid_from, so as_of queries reflect when things were true, not when they were imported. A value that is not ISO 8601 returns 422. |
Any other field is refused with 422 invalid_request (for example userId: Extra inputs are not permitted): field names are snake_case.
Example
from korely_memory import Korely
korely = Korely(api_key="kor_live_...")
# Store a plain-text memory scoped to one end userresult = korely.add( "Northwind Hosting costs 50 euro per month since the June upgrade.", user_id="customer-4812", agent_id="infra-bot", metadata={"source": "slack", "channel": "#billing"},)
# Returns are dataclasses, use attribute access (not result["id"])print(result.id) # mem_8f2c1aprint(result.status) # processing: facts are extracted a few seconds laterprint(result.facts) # []
# A few seconds laterfacts = korely.get_facts(entity="Northwind Hosting", user_id="customer-4812")print(facts[0].predicate, facts[0].object) # costs 50 euro per month
# You can also pass a chat message list, the pipeline extracts facts from the threadkorely.add( [ {"role": "user", "content": "I just upgraded to the Advanced plan."}, {"role": "assistant", "content": "Great, I have updated your profile."}, ], user_id="customer-4812", agent_id="infra-bot",)Response
{ "id": "mem_8f2c1a", "content": "Northwind Hosting costs 50 euro per month since the June upgrade.", "user_id": "customer-4812", "agent_id": "infra-bot", "run_id": null, "metadata": { "source": "slack", "channel": "#billing" }, "status": "processing", "created_at": "2026-06-15T09:14:00.412345+00:00", "updated_at": null, "facts": []}Response fields
| Field | Type | Description |
|---|---|---|
id | string | Unique memory ID (prefix mem_). Use this to retrieve, update, or delete the memory later. |
content | string | The text that was stored, with leading and trailing whitespace removed. |
user_id | string or null | The end-user scope passed in the request, or null if none was provided. |
agent_id | string or null | The agent namespace passed in the request, or null for the default namespace. |
run_id | string or null | The session scope passed in the request, or null if none was provided. |
metadata | object | The arbitrary key-value pairs passed in the request, returned verbatim. Empty object if none were set. |
created_at | ISO 8601 string | UTC timestamp of when the memory was first stored. |
status | string | processing on the immediate response; ready once the facts are extracted (or error). |
updated_at | ISO 8601 string or null | null on a fresh add; the UTC timestamp of the last edit once the memory is updated. |
facts | array | Typed facts extracted from this write. Extraction is asynchronous, so on the hosted service this array is empty on the immediate response; the facts land a few seconds later (read them with get or get_facts), or never if the content carries no extractable facts. The fields below describe each fact once present. |
facts[].id | string | Unique fact ID (prefix fct_). Appears in the sources of get_context() and in the invalidated arrays of later facts that supersede it. |
facts[].subject | string | The entity this fact is about (e.g. a product, person, or organisation name). |
facts[].predicate | string | The relationship or attribute, normalized to a canonical verb (e.g. costs, works_at, likes). When you read facts back, the original surface verb is preserved in predicate_raw. |
facts[].object | string | The value of the relationship (e.g. 50 euro per month). |
facts[].predicate_family | string or null | A coarse bucket for the predicate (e.g. financial, people, places, preferences). A relation the vocabulary has not seen before gets its family from the model, once per project, and other only when none of the ten fits. Filter on this on the facts endpoint to group related facts. |
facts[].valid_from | ISO 8601 string or null | UTC timestamp when this fact became true (bi-temporal valid_time axis). The request's timestamp when you pass one; otherwise the date the text states when it writes the year ("since March 2026"), never a future one; otherwise the time of the write. |
facts[].invalidated | array of strings | IDs of earlier facts that this write superseded via the two-stage contradiction check. Empty array if no contradiction was detected. Superseded facts are never deleted, they stay queryable, and a read of those facts shows their invalid_at timestamp set. |
facts[].tense | string | current, past or planned, as the text said it. |
facts[].invalid_at | ISO 8601 string or null | Set when the text states the fact as finished (a past fact); null otherwise. |
facts[].observation_count | integer | How many memories assert the fact. Above 1 when this write restated a fact the store already held: it was reconfirmed, not duplicated. |
Each fact carries a normalized predicate and a
predicate_family bucket (some predicates fall into
other rather than a specialised family). When the pipeline
detects a contradiction with an earlier fact for the same subject and scope,
the older fact's id appears in the new fact's invalidated array.
The old fact is never deleted, read it back through
get_facts with
include_invalidated=true and you will see its invalid_at
timestamp set. This invalidate-don't-delete model, with
valid_from on every fact, is what lets you reconstruct any past
state with as_of.
Errors
Every error response is the same flat envelope,
{"code": "<slug>", "message": "<text>"}. There is no
error or detail field; the slug in the table below is
the code value.
| Status | Code string | When it occurs |
|---|---|---|
401 | invalid_key | The Authorization header is missing, malformed, or the key has been revoked. |
403 | forbidden | The key lacks the memories:write scope. |
403 | agent_cap_exceeded | The agent_id in the request is a brand-new name that would push your total agent count past your plan's limit (2 on Hobby, 10 on Developer, 100 on Team, 500 on Scale). Nothing is written or counted. Reuse an existing agent_id or upgrade your plan. |
422 | invalid_request | Request validation failed: content missing, not a string (an array is a 422 on the REST API), blank or over 16,000 chars; user_id or agent_id breaking the id rule; run_id over 255; metadata over 8,192 bytes; timestamp not ISO 8601; malformed JSON; an unknown field. The body is the flat {"code","message"} envelope, with the offending field named in the message (e.g. "content: Field required"). |
429 | quota_exceeded | Your account's monthly writes are used up, 10% grace included; nothing is written. The body adds limit (the plan's figure, top-ups included), used and resets_at (00:00 UTC on the 1st) to code and message (e.g. "Monthly write limit reached: 250 writes on the Hobby plan this month, plus a 10% grace. It starts again on 2026-11-01. To go on now, ..."). No Retry-After header: read resets_at. |
429 | rate_limit_exceeded | Too many requests in the current minute, hour or day. Carries a Retry-After header in seconds. |
Notes
- Not idempotent. Each call to
add() creates a new memory object. If you call it twice with identical content, two separate memories are stored; the facts they state are reconfirmed (observation_count 2), not duplicated. Deduplicate on the caller side before writing, or use update to replace an existing memory in place. - Scoping is additive.
user_id, agent_id, and run_id are independent filter axes. A read returns the memories that match every scope you pass: a search with only user_id finds that user's memories whatever their agent_id or run_id, and adding run_id narrows it to one session. Omitting a scope on read broadens the search to everything the key can see. - End users are unlimited. Any number of distinct
user_id values is allowed on every plan. The per-plan limit applies only to agent_id namespaces (distinct apps you register). - Write quota and rate limits. Each successful
add() call counts one write per 6,000 characters of its content against your monthly quota: one for anything up to 6,000 characters, 3 for the longest memory the API takes. Once the quota is exhausted the API returns 429 quota_exceeded (with no Retry-After header, the quota resets on the 1st of the month, UTC). Bursty traffic can separately hit a per-minute, per-hour or per-day rate limit, which returns 429 with a Retry-After header in seconds. - Fact extraction is pipeline-driven. The number of facts returned varies with content length and density. Very short strings (under one sentence) may return an empty
facts array. Contradiction detection only runs when the same subject-predicate pair already exists for the same user_id scope.
Related
- Get context, the primary recall path. Assembles the active typed facts plus relevant memories for a user into one ready-to-inject block. This is the differentiator: structured, contradiction-resolved recall, not just nearest-neighbour snippets.
- Search memories, secondary recall. Semantic vector search (cosine similarity) returns the closest raw memory snippets for a query.
- Update a memory, replace a memory's content and re-run fact extraction, with optional optimistic concurrency.
- API reference, full contract for all
/v1 endpoints.