Korely

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
The write pipeline triggered by add(). All stages run on Korely infrastructure.

Request

Endpoint: POST /v1/memories. SDK: korely.add(content, *, user_id=None, agent_id=None, run_id=None, metadata=None, timestamp=None).

ParamTypeNotes
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 user
result = 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_8f2c1a
print(result.status) # processing: facts are extracted a few seconds later
print(result.facts) # []
# A few seconds later
facts = 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 thread
korely.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

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

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