Core memory operations
Update a memory
Replace a memory's text. Korely extracts the facts again from the new text: a changed value closes the old fact, a restated fact stays open, and a fact the new text no longer states is closed.
/v1/memories/{memory_id}
SDK: korely.update(memory_id, content=...). The edit
replaces the whole text of one memory
and runs the extraction again on
it, with the same contradiction
check as a new write. Three things can happen to an
open fact of the memory:
the new text restates it (it stays open), the new text gives the same
subject and relation another value (it is closed, and its
invalidated_by names the new fact), or the new text no longer states it (it is
closed at the time of the edit, unless another memory also states it). You
write no code to close old facts. The guide
shows the call with a runnable example.
Authentication
HTTP header, required: Authorization: Bearer kor_live_.... The key must carry the memories:write permission.
Path parameter
| Parameter | Type | Required | Description |
|---|---|---|---|
memory_id | string | Required | The memory id (mem_ and 32 hex characters) to update, as Add a memory returned it. A malformed id returns 404. |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
content | string | Required | New content. 1-16,000 characters. Stripped and rejected if blank or whitespace-only (422). |
expected_updated_at | string | Optional | Version check: the updated_at you last read, or created_at if updated_at is null, sent back exactly as the API returned it. The edit is written only if the memory is still at that version, to the microsecond, when it is written; otherwise 409 stale_write and nothing changes. A value that cannot be read as a date, the empty string included, is a 422. A date with no time of day, or a time cut to the millisecond, is another version: 409. Leave the field out for an edit without the check. |
Any other field is refused with 422 invalid_request (a memory's user_id, agent_id, run_id and metadata do not change with an edit). Field names are snake_case.
Example request
curl -X PATCH https://api.korely.ai/v1/memories/mem_8f2c1a \ -H "Authorization: Bearer kor_live_..." \ -H "Content-Type: application/json" \ -d '{ "content": "Giulia now prefers in-person standups.", "expected_updated_at": "2026-03-01T09:14:22.418093+00:00" }'Response
200 OK. The updated memory. On the Cloud the extraction runs
after the reply, so the response always carries status processing and an empty facts list; the facts
follow about ten seconds later. A Self-hosted install started with
WRITE_ASYNC=false (an environment variable of the server) extracts inside the call and returns them.
{ "id": "mem_8f2c1a", "content": "Giulia now prefers in-person standups.", "user_id": "customer-giulia-4812", "agent_id": "onboarding-bot", "run_id": null, "client": null, "kind": "note", "metadata": {"source": "slack"}, "status": "processing", "created_at": "2026-03-01T09:14:22.418093+00:00", "updated_at": "2026-03-02T11:02:10.183112+00:00", "facts": []}| Field | Type | Description |
|---|---|---|
id | string | The public memory id (mem_). |
content | string | The updated content, echoed back. |
user_id | string · null | The end user this memory belongs to (null if none). |
agent_id | string · null | The agent (agent_id; null if none). |
run_id | string · null | The run (run_id; null if none). |
client | string · null | The tool that wrote it, from the X-Korely-Client header of the write or the name an MCP client gave when it connected; a label, not a namespace. |
kind | string | note, or session for the log of a work session (the kind of the write). |
metadata | object | The metadata dict stored with the memory. |
created_at | string · null | ISO 8601 timestamp of the first write. It never changes. |
updated_at | string · null | ISO 8601 timestamp of this edit. It changes on every edit, even when the text is the same. |
status | string | The memory's status: processing while the new facts are being extracted, then ready (or error, see When extraction fails). The edit answers processing as POST /v1/memories does. |
facts | array | Always [] on the Cloud. A Self-hosted install with WRITE_ASYNC=false fills it with the facts of the new text, in the write shape of Add a memory. Read the facts later with Get a memory or Get facts; a fact the edit closed shows invalid_at and, when a new fact replaced it, invalidated_by. |
Errors
| Status | Code | Cause |
|---|---|---|
401 | invalid_key | Missing or invalid kor_live_ key, Invalid or missing API key: ..., then what is wrong. |
429 | quota_exceeded | An edit extracts again and counts the writes of its new text (one per 6,000 characters, at least one): past the monthly write quota it is refused and nothing changes. An edit that ends in 404 or 409 counts nothing. |
403 | forbidden | The key lacks the memories:write permission; message names it. |
404 | not_found | Malformed memory_id, or no such memory in this key's project (including when checked against expected_updated_at), Memory not found. |
409 | stale_write | expected_updated_at was provided but doesn't match the current record, expected_updated_at does not match the current record. |
413 | payload_too_large | The body is larger than 2,097,152 bytes. Refused before it is read; nothing changes. On the Cloud a body over 1 MB gets a bare 413 first, without this code. |
422 | invalid_request | content is empty or whitespace-only, or its length is outside 1-16,000 characters; or expected_updated_at cannot be read as an ISO 8601 date or time; or the body has a field other than content and expected_updated_at. |
429 | rate_limit_exceeded | Past the rate limit. Wait as long as Retry-After says. |
408 | request_timeout | The body did not arrive whole within 30 seconds. Nothing changes; send it again. |
500 | internal_error | The server failed on this request; the message holds no detail. Read the memory again before you retry. |
503 | unavailable | The database did not answer, or another change to the same records held this request more than 5 seconds. Nothing changes. Retry after the seconds in Retry-After. |
Notes
- It's a write. Update requires the
memories:writescope and does not meter the read query quota. - On a memory in
error. An edit puts the memory back in the queue, and the extraction runs again on the new text. If it fails again, the memory is inerroragain and its old facts stay open. - A new value closes the old fact. If the new text gives a fact a new value, the old fact is closed (
invalid_atis set,invalidated_byis the new fact), even when another memory still states the old value. A fact the new text no longer states is closed at the time of the edit, unless another memory also states it. Write the new text as a current state (“costs 55 euro per month”), not as a story of a change. - An empty extraction closes the facts. If the model finds no fact in the new text, the edit closes every fact of the memory. The memory is still
readyand no error is reported, so read the facts after an edit. Since 1.0.9, an edit with the text the memory already has, once extracted, changes nothing: no model is called, no fact is written or closed, and the reply is the memory as it is. It still counts as a write against your monthly writes. Facts that such an edit closed before 1.0.9 stay closed. A text that changed by only a comma is read again, and if that read finds no fact it closes the facts, because Korely cannot tell it from an edit that took the value away. - When the facts start. A new fact's
valid_fromis thetimestampof the original add if it had one, otherwise a date the text states, otherwise the time of the edit. The edit cannot change the timestamp. - History. Memory history keeps one
updatedevent: the last edit. - Optimistic concurrency.
expected_updated_atis compared to the microsecond, with no tolerance: two edits made from the same read, even in the same second, cannot both win; the second gets409 stale_write. The check holds at the write itself, so an edit that lands between your request's check and its write is not overwritten either. Send back the string you read: a value rounded to the millisecond (a JavaScriptDate, for example) is another version. A value that cannot be read, or an empty one, is refused with422 invalid_request, and nothing changes; a date with no time of day is another version and gets the409. - Webhooks. The PATCH emits no
memory.created. When re-extraction runs, each fact it closes firesfact.invalidated:invalidated_byis the new fact, ornullwithreason: "memory_updated"for a fact the new text no longer states.
Related
- Update a memory, guide, the narrative walkthrough with context.
- Add a memory, store a new memory.
- Get facts, read back the re-extracted typed facts.