Skip to content

Memory through the API

On this page you write and read memory records through the API and give a turn the memory tools. Use memory to retain individual facts across calls, such as a customer preference or an earlier decision. Use knowledge to ingest and search documents. An administrator reads the records in Memory in the console.

Your key needs the memory capability, and knowledge, which memory depends on. Create a knowledge index to hold the records before writing memory.

Configure the memory capability

An administrator sets these fields on the key's memory capability, in the console or through PATCH /tenants/{id}/keys/{id}; see Capabilities per key.

Field Meaning
retrieval_tool whether a run may call the memory search tool. Defaults to true
write_tool whether a run may call the memory write tool. Defaults to true
injection.enabled whether matching records are added to context automatically, with no tool call. Defaults to false
injection.index_id the index automatic injection draws from; required when injection.enabled is true
injection.types record types eligible for injection; empty means every type
injection.metadata a metadata filter an injected record must match
injection.min_confidence the minimum confidence a record must reach to be injected, 0 to 1. Defaults to 0
injection.limit the most records injected per run, 1 to 100. Defaults to 20
injection.budget_tokens the most tokens the injected block may cost, 100 to 8,000. Core counts 4 characters per token over every rendered line, fields included. Defaults to 1500; a store's own budget and your operator's ceiling can only lower it
decay a half-life in days per record type; a type absent here never decays. See confidence decay

retrieval_tool and write_tool gate the two memory tools; injection governs automatic injection.

Write a record

curl -s -X POST "$CORE_URL/memory" \
    -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
    -d '{"index_id": "idx_8Bd3...", "type": "preference",
         "text": "Prefers email over phone.",
         "metadata": {"customer": "acme"}, "confidence": 0.9}'
Field What it does
index_id which index this record lives in
type your own label. Core stores it, filters on it, and never interprets it
text or structured the content; exactly one of the two
metadata key/value strings you filter on later
confidence 0 to 1, defaulting to 1. See decay

An unavailable index returns 404 -- including an index your key’s policy does not name, since a record lives in an index and is as absent as the index is. A write denied by the key’s policy returns 403 and is recorded in the audit history. Read and write permissions are separate, so a key may be able to search memory without adding records.

Call Answers
GET /memory records, filtered by index_id, type, metadata, min_confidence, and created_after and created_before (ISO 8601 instants with an offset; a record created at created_after is included, one created at created_before is not; reversed, they answer 422). An administrator's GET /tenants/{id}/memory takes the same filters
GET /memory/{memory_id} one record
PATCH /memory/{memory_id} change metadata or confidence, or a commitment's status, due_at or owed_to
DELETE /memory/{memory_id} 204

An administrator can also read your organisation's memory in the console, in the Memory section; every record shown there is across every index, and every read is recorded in the audit trail.

Use memory tools

{
  "model": "<a model id>",
  "tools": [{ "type": "tool", "name": "memory__idx_8Bd3...__search" }],
  "input": "Draft a follow-up for Acme."
}

Each index has two tools, named after the index's own id:

  • memory__<index id>__search — find records by question, with an optional types filter and an optional creation window, after and before (ISO 8601 instants with an offset, as above).
  • memory__<index id>__write — store something the turn learned. It takes only the types the store knows: the extraction switch's types when the store has one, else fact, preference, commitment and behaviour, never episode; another type is refused and the model is told the allowed ones. The tool also tells the model today's date in the turn's zone -- your request's timezone, else your organisation's, else UTC -- so a due date it works out from "next Friday" lands on the right day.

The key’s memory configuration decides which of the two it may be offered; asking for one it may not have answers 400 on that tools[] entry. GET /tools?source=memory lists both for every index you have.

What the write tool answers is a JSON object, the same text the model reads and a client sees as the tool result item on the thread (and as the tool call's result on the stream):

Key Meaning
status stored for a new record, confirmed when the person already held a current record saying exactly the same, updated for a change to a commitment
memory_id the record stored, confirmed or changed
supersedes only for a correction: the id of the record it ended

To undo a save, call DELETE /memory/{memory_id} with that id. The key needs the same memory access as for the other record routes, and a subject token (or the X-Core-Subject header) deletes only that person's own records. Deleting a confirmed record removes the older record that was confirmed, not a new one; deleting a correction's record does not bring back the record it ended.

Search results include each record's confidence as a whole percentage, such as confidence 82%, and contested when maintenance marked the record as contradicting another. They are a tool result, so the model is told they are data when your key holds trust_envelope. Memory written during a turn records the message chain it came from, so you can inspect the source of a remembered fact.

Automatic memory injection

When injection is enabled in the key’s memory configuration, Core adds matching records to the context before the model runs. Selection uses record type, metadata and a minimum confidence value. The model does not need to call a search tool. The model reads every injected record as data, one line of JSON per record, and never as an instruction, whoever wrote the record.

A turn is given the records of the person it is for, and the shared ones, never another person's. A turn that names nobody is given the shared records only: name the person with X-Core-Subject or user when the turn is theirs.

The records stand in one block under labels only Core writes, in this order: [commitments] (open commitments, the person's and the shared ones, due soonest first, at most the store's commitments.inject_limit), [behaviour] (the person's own rules for how the assistant should act), [about_this_person], [organisation] (the shared records) and, when the store asks for them, [past_sessions]. A section with no record has no label. Each line is one JSON object whose first field after type is the record's confidence as a whole percentage, for example {"type":"preference","confidence":"82%","text":"prefers mornings"}; a record maintenance marked as contradicting another also carries "contested":true. Core adds no sentence about what to do with either, so say it in your agent's instructions if you want a rule.

A record of type behaviour is a rule for how the assistant should act for one person, such as "answer in bullet points", "reply in Dutch" or "ask before booking"; what she likes is a preference. Every turn for her is given her own current rules, newest first, at most ten, under [behaviour], whatever the turn is about. A turn for somebody else, or for nobody, is never given them, and a shared behaviour record is never injected. Write one like any record, with POST /memory or the write tool, or let extraction find them. The key's types, metadata and min_confidence narrow them as they narrow every record.

Each section is ranked on its own by confidence, then by how often the record was recalled, then by id: recall only breaks a tie and never raises a confidence. A store can rank by relevance instead, with its rank setting. injection.limit bounds the records of the person and the shared ones together, not commitments or behaviour rules, and the token budget bounds the block. Both apply and the stricter wins: the count stops the walk, while a record that does not fit the budget is skipped and the next one is tried. The shared records may use at most 40% of the budget on a turn for a person, so a large shared store cannot crowd out her own records. The turn's trace says which bound left records out: dropped_by_count and dropped_by_budget, and the tokens the block cost (estimated_tokens).

A store can also hand a turn its past sessions: the episodes nearest what the turn is about, newer ones ranked up, at most the store's episodes_limit. They stand last, under [past_sessions], so they never take the place of a commitment or a fact. A turn for a person gets her own episodes only, and a turn that names nobody the shared ones. Each line gives the session's span and the thread and first item to read its messages from, for example {"type":"episode","confidence":"100%","from":"2026-10-01T09:00:00+00:00","to":"2026-10-01T09:30:00+00:00","thread_id":"thr_01J…","first_item_id":"itm_01J…","text":"Planned the move to Delft."}. The key's metadata filter narrows them as it narrows every record, and a key that names types gets them only when episode is among them.

Give each assistant its own store

One key can serve several assistants, each an agent, and each can draw on a store of its own. Give the agent version a memory_injection block, in the same shape as the key's memory.injection:

{"name": "billing-desk", "model": "your-chat-model",
 "memory_injection": {"enabled": true, "index_id": "idx_billing", "types": ["fact", "preference"]}}

A turn on that agent uses the agent's block instead of the key's, as a whole: its store, types, metadata filter, minimum confidence, limit and budget. Nothing is taken from the key's block, so you can tell from the agent which store a turn draws from. "enabled": false gives the agent's turns no injected memory, even when the key injects. An agent without memory_injection uses the key's block as before. The key still needs the memory capability: without it nothing is injected, whatever the agent says. When you save the agent, its store must be an index of your organisation that your own policy names, or the save is refused with 422 on memory_injection.index_id. The calling key's policy must name the agent's store too, or its turns are given nothing, without an error. Everything above holds for the agent's store as for the key's: the person's own records and the shared ones only, the store's own settings, and the token budget.

Settings per store

An administrator can lower and divide the budget for one store, choose how its records are ranked and let it hand a turn its past sessions, with PATCH /memory/stores/{index_id}, in its injection section. Sending the section replaces it whole.

Field Meaning
person_limit the most records of the person injected from this store, 1 to 150. No value leaves the key's injection.limit the only count
shared_limit the most shared records injected from this store, 1 to 150. No value leaves the key's limit the only count
budget_tokens a token budget for this store, 100 to 8,000. It can only lower the key's injection.budget_tokens
shared_budget_share the largest share of the budget the shared records may use on a turn that names a person, above 0 and up to 1. Defaults to 0.4
episodes_limit the most past sessions a turn is handed, 0 to 5, under [past_sessions]. Defaults to 0, none. A turn with no query to rank by gets none, and asking for them makes Core embed the turn's text even when rank is confidence
rank the order records are added in before the budget cuts them: confidence (the default), relevance to what the turn is about, or blend, which combines the two. Under relevance and blend, Core embeds the turn's text once, shared with skill selection by retrieval when that also asks, and records without an embedding come after the records that have one. Commitments always keep their due order. A turn with no query to rank by (no user text, no embedding model, or text that cannot be hidden) is ranked by confidence

Extract memory from conversations

A memory store can fill itself from your organisation's own conversations. It is off by default. Switch it on with PUT /memory/stores/{index_id}/extraction, naming the chat model that reads the conversations, your own instructions on what is worth remembering, the record types it may write and how often it runs:

{"enabled": true, "model": "your-chat-model",
 "types": ["fact", "preference", "commitment", "behaviour"], "schedule_minutes": 60}

Those four types are the default when you leave types out.

Core then reads every thread that names a person and has been quiet for at least half an hour, in windows of its messages, and writes what it finds as records of that thread's person. Which threads are read is the switch's threads field: with_person (the default of a PUT that leaves it out) skips a conversation that names nobody, since that would write one person's private facts as shared records every colleague is given; all reads every thread, and a GET of a switch set earlier shows all. A fact said again confirms the record you have, and a correction replaces the record it corrects, which stays readable in its history. A thread of a key that hides personal data is hidden again when it is read: the extraction model sees the same kind of placeholders the conversation's own model saw, whether or not anything was found to hide at the time, and the record holds the real values, as a memory written during a conversation does. When the personal data cannot be hidden, that window is not read and its failure shows on the switch. Core keeps on a thread that a hiding key wrote in it, from the first such turn made after your deployment began recording this; ask your operator when that was. An older thread is judged by what is left of it: one in which Core hid a value is never read, one in which nothing was found is hidden under its key's settings as long as that key exists, and it is read as written once that key has been deleted. Your key needs the threads capability and a policy that allows writing memory; the model's tokens are metered like any other call. GET on the same path shows the switch, its last run and how many records it wrote, confirmed and replaced. Send "enabled": false to stop it; what it already wrote stays.

The same GET tells you whether extraction is working. windows counts the conversation windows that were read (ready), that failed (failed), and that are still waiting (queued, or parked at your monthly cap), and error_code names the most common reason among the failed ones: for example invalid_model_output when the model cannot answer in the form Core asks for. state is active when all is well, off when the switch is off, and otherwise says why nothing runs: author_unavailable (the key that set the switch was revoked or has expired), model_unavailable (the model is no longer offered to your organisation) or objects_missing (the switch's connector was deleted, or its index runs another pipeline). Send the switch again to fix author_unavailable or objects_missing; for model_unavailable, send it with a model your organisation is offered, because the same model is refused with 422 on model. The key that sends the switch becomes the key extraction runs as, for the windows already copied as well. state checks only these three things; a key that still exists but lost threads or the permission to write memory leaves state at active, and you see the failures in the last run and in windows. When you change the model, the instructions or the types, Core reads the windows that failed once more; windows that were already read are left alone.

Sending the switch needs threads whether you switch it on or off. Reading also stops when you delete the switch's connector or the store's index, and neither of those needs threads. filters.key_ids limits reading to conversations of the keys you name, and filters.agent_ids to conversations in which one of the agents you name answered, so each assistant can fill a store of its own; with both, a conversation must match both. An id that is not an agent of your organisation is refused with 422 when you switch extraction on; an id the switch already held is not checked again. While a switch names an agent, deleting that agent is refused with 409 naming the store; remove it from the switch's filters first. A key that was revoked or has expired is accepted, because its conversations are still yours; an id that was never a key of your organisation is refused with 422 when you switch extraction on. Core recognises a deleted key by your organisation's audit trail of keys, so a key deleted longer ago than that trail is kept is treated as never issued.

Keep a record of each session

With "episodes": true in the switch, extraction also writes one record of type episode for each session of a thread that has settled, a run of messages that ends in a quiet spell. An episode is a short account of what happened: a text that says what was going on, what was done, how it came out and what stayed open, the span of time it covers, and the thread and the first and last message it was made from, so a caller can go on to the messages themselves. Every episode also says in its structured part whether the session went well: result is succeeded, failed or unknown, and result_basis says what decided it -- how the person took it when they showed it (reaction), else a last response that failed or stopped short (records), else the model's own reading of whether the person got what they wanted (judge). Beside it, response_ids names the responses of the session, agent_id and agent_version the agent that ran it, and tools each tool result by its name and status alone. An episode written before these were added has none of them. It also says, in one short sentence, what the person wanted from the session (intent, left out when unclear), and its text opens with that wish, so a search by what someone wanted finds it. The episode belongs to the thread's person, or to the organisation when the thread names nobody, and it is data like any extracted record, with the trust of the messages it rests on. It may also tell which tools the session used: the model reads a short excerpt of each tool call and result between the messages, at most 300 characters of each. Reasoning and sealed messages are never read.

The switch counts only from the moment you set it: a session that settled before is not written back, so switch it on early. It uses the same model and costs tokens like the rest of the extraction. You cannot write an episode yourself: POST /memory and the write tool refuse type episode with 422. A turn is handed episodes with the store's episodes_limit, and the lessons a store can draw rest on them. With "lessons": true as well, the same reading of a session can also note what it teaches; see Lessons from a single session.

Let a person's sessions teach her assistant

A person's own sessions can teach her assistant. When at least min_episodes of her episodes went alike, all well or all badly, the store's maintenance pass asks your organisation's helper model for one short sentence: a way of working that went well, or a pitfall to avoid. Core keeps that sentence as a memory record of type lesson that is hers alone: untrusted, written by the model, never promoted to the organisation and never shown to a colleague. What is sent to the model is pseudonymised, and nothing is sent for a person whose rules keep the helper model from reading her conversations.

A lesson shows in her own turns, under [about_this_person] in the injected block, like any record of hers. A key whose injection types is empty gets it, and so does one that names lesson; a key that lists other types only does not. A lesson is never approved first: it is written and used.

Turn it on per store in its lessons section with PATCH /memory/stores/{index_id}. Like the other sections but promotion, it needs the memory capability alone. Each lesson is a metered model call, so the section is off by default. It also runs only while the store's maintenance.enabled is on, which it is unless you switched maintenance off in the store's Maintenance card.

Field Meaning
enabled whether the maintenance pass writes lessons for this store's people; off by default
min_episodes how many of one person's alike episodes a lesson rests on, 2 to 10. Defaults to 3
similarity how alike two episodes must be, as cosine similarity from 0.5 to 1. Defaults to 0.8
max_per_pass the most lessons asked for in this store per pass, 1 to 20. Defaults to 5
curl -s -X PATCH "$CORE_URL/memory/stores/$INDEX_ID" \
    -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
    -d '{"lessons": {"enabled": true}}'

Lessons from a single session

One session can teach something too. Send "lessons": true in the extraction switch, next to "episodes": true (without episodes the switch is refused with 422), and the model that writes a session's episode may also note up to three lessons in the same call, at no extra model call: a way of doing a task that is worth reusing (workflow), or a mistake and how it was put right (pitfall). Most sessions teach nothing (a question answered, small talk) and then none is written. Each lesson is a lesson record of the session's person, with the trust of the messages it rests on; a thread that names nobody gets no lesson. Its metadata says which kind it is (lesson.kind) and what judged how the session went (lesson.basis): reaction when the person showed it went well or badly, model when the model judged by the outcome. A lesson she already has in the same words is confirmed instead of written twice; one in nearly the same words is too, but only when the store's index embeds its records and both lessons were embedded. It is off by default.

Lessons reach her turns like any record of hers. With the store's injection rank at relevance or blend, the lessons closest to what the turn is about come first.

{"enabled": true, "model": "your-chat-model", "episodes": true, "lessons": true}

Make a personal fact the organisation's

A fact in one person's memory can be the organisation's: "expense claims go to finance by the 25th" said in Jan's conversation is about the organisation, not about Jan. The improvement analyses can propose such a fact as a shared record, when several people's memories hold it alike or when the analysis model judges one person's record general knowledge. A fact about a person, or one holding an address, a phone number or a link, is never proposed. Turn it on per store in its promotion section with PATCH /memory/stores/{index_id}:

Field Meaning
enabled whether the analyses read this store's personal records; off by default, since each reading is a metered model call
apply_without_review inherit (the default) follows the organisation's apply_level for memory_shared_fact; on (publish) or off (propose) decides for this store
min_people how many people must hold the same fact, 2 to 20; defaults to 3
types the record types read, ["fact"] by default; never preference, commitment, episode, lesson or behaviour
labels the metadata a proposed shared record starts with, such as {"audience": "everyone"}

Only a key holding management beside memory, or an administrator in the console, may write this section, and only when your organisation has the improvements capability; anyone else gets 403. The other sections of a store need memory alone. The analyses themselves are switched on in the improvement settings.

curl -s -X PATCH "$CORE_URL/memory/stores/$INDEX_ID" \
    -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
    -d '{"promotion": {"enabled": true, "labels": {"audience": "everyone"}}}'

Applying a proposal writes a new shared record for nobody in particular; the person's own record stays hers. Erasing a person also deletes a proposal that rests on her record alone.

Confidence decay

The stored confidence remains unchanged until you edit it. If decay is configured, readers receive an effective value based on the time since the last update:

effective = confidence * 0.5 ** (age_days / half_life_days)

The key configuration specifies a half-life for each record type that should decay. Types without a half-life do not decay. GET /memory?min_confidence=0.5 filters on the effective value. Any PATCH restarts the age calculation.

Your application decides how to use low-confidence records, for example by excluding them or asking the user to confirm them. Core calculates the score without taking those actions.