Give an agent long-term memory
By the end of this tutorial you will have an agent that remembers what a customer told it — proven by a later, separate generation that uses the fact without being told again.
Six steps:
- Create a memory store — where the facts live.
- Add a rule that learns from the agent.
- Let the agent recall from the store.
- Tell the agent a fact.
- See what it learned.
- Ask in a new conversation.
Every step is one API call, shown for all three clients. The ids in the responses are examples — copy the ones your own calls return.
Prerequisites
-
A credential. A
nat_sk_…API key (or a session JWT from Auth) exported asNATURALI_TOKEN, and your client set up — the CLI, the SDK or plaincurl. -
A working agent. That is what Your first agent generation builds. Arrive here with both ids exported:
export NATURALI_TOKEN=nat_sk_...export PROJECT=proj_V1StGXR8Z5jdHi6Bexport AGENT=agent_e6iR8kjbMsGPbpMR
Every fact is embedded when it is written, and embeddings are paid from your credit balance on every plan. Memories also count towards your account's storage allowance — about 4 kB each, whatever the text.
1. Create a memory store
A memory store is the scope the facts are kept in. One store per agent keeps what this agent learns out of every other agent's answers.
- CLI
- SDK
- curl
naturali create-memory-store \
--project-id "$PROJECT" \
--name customer-notes \
--description "What customers tell the bakery agent about themselves."
const { data: memoryStore } = await naturali.memoryStores.createMemoryStore({
path: { project_id: process.env.PROJECT! },
body: {
name: 'customer-notes',
description: 'What customers tell the bakery agent about themselves.',
},
});
curl -X POST "https://api.naturali.ai/v1/projects/$PROJECT/memory-stores" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "customer-notes",
"description": "What customers tell the bakery agent about themselves."
}'
{
"id": "mstore_EisX1nAWTwPPwPBd",
"project_id": "proj_V1StGXR8Z5jdHi6B",
"name": "customer-notes",
"description": "What customers tell the bakery agent about themselves.",
"duplicate_threshold": null,
"supersede_threshold": null,
"created_at": "2026-10-03T08:40:35.447Z"
}
export STORE=mstore_EisX1nAWTwPPwPBd
2. Add a rule that learns from the agent
A memory rule reads every completed turn of the agents it names and writes the facts it finds into the store. With no handler set, the built-in extractor does the reading — one extra model call per turn.
- CLI
- SDK
- curl
naturali create-memory-rule \
--project-id "$PROJECT" \
--memory-store-id "$STORE" \
--on agents.generation.completed \
--source-agent-ids "$AGENT"
const { data: memoryRule } = await naturali.memoryRules.createMemoryRule({
path: { project_id: process.env.PROJECT! },
body: {
memory_store_id: process.env.STORE!,
on: 'agents.generation.completed',
source_agent_ids: [process.env.AGENT!],
},
});
curl -X POST "https://api.naturali.ai/v1/projects/$PROJECT/memory-rules" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"memory_store_id\": \"$STORE\",
\"on\": \"agents.generation.completed\",
\"source_agent_ids\": [\"$AGENT\"]
}"
{
"id": "mrule_q6pmIxo0nIEaluOt",
"memory_store_id": "mstore_EisX1nAWTwPPwPBd",
"on": "agents.generation.completed",
"source_agent_ids": ["agent_e6iR8kjbMsGPbpMR"],
"agent_id": null,
"tool_id": null,
"ai_provider_id": null,
"model": null,
"enabled": true
}
The rule lives on the store, not the agent: the agent gains no tool and its configuration does not change. Writing is the platform's job after each turn, and a rule never fails the turn it reads.
3. Let the agent recall from the store
Writing is half of memory; the agent also has to read. memory_store_ids in
knowledge_config searches the
store before every generation, with the user's message as the query, and puts
what matches in the agent's context. Set the instructions in the same call so
the agent uses what it recalls and does not invent the rest.
- CLI
- SDK
- curl
naturali patch-agent \
--project-id "$PROJECT" \
--agent-id "$AGENT" \
--instructions 'You answer customers of a bakery. Use what you remember about the customer. Do not name products you were not told about. Answer in one sentence.' \
--knowledge-config "{ \"memory_store_ids\": [\"$STORE\"], \"limit\": 3 }"
const { data: agent } = await naturali.agents.patchAgent({
path: { project_id: process.env.PROJECT!, agent_id: process.env.AGENT! },
body: {
instructions:
'You answer customers of a bakery. Use what you remember about the customer. Do not name products you were not told about. Answer in one sentence.',
knowledge_config: { memory_store_ids: [process.env.STORE!], limit: 3 },
},
});
curl -X PATCH "https://api.naturali.ai/v1/projects/$PROJECT/agents/$AGENT" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"instructions\": \"You answer customers of a bakery. Use what you remember about the customer. Do not name products you were not told about. Answer in one sentence.\",
\"knowledge_config\": { \"memory_store_ids\": [\"$STORE\"], \"limit\": 3 }
}"
{
"id": "agent_e6iR8kjbMsGPbpMR",
"instructions": "You answer customers of a bakery. Use what you remember about the customer. Do not name products you were not told about. Answer in one sentence.",
"knowledge_config": {
"limit": 3,
"memory_store_ids": ["mstore_EisX1nAWTwPPwPBd"]
},
"version": 2
}
4. Tell the agent a fact
- CLI
- SDK
- curl
naturali create-agent-generation \
--project-id "$PROJECT" \
--agent-id "$AGENT" \
--wait true \
--messages '[{"role":"user","content":"Hi, I am Ana. Please note that I am allergic to almonds."}]'
const { data: told } = await naturali.agents.createAgentGeneration({
path: { project_id: process.env.PROJECT!, agent_id: process.env.AGENT! },
query: { wait: true },
body: {
messages: [
{
role: 'user',
content: 'Hi, I am Ana. Please note that I am allergic to almonds.',
},
],
},
});
curl -X POST \
"https://api.naturali.ai/v1/projects/$PROJECT/agents/$AGENT/generate?wait=true" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "messages": [{ "role": "user", "content": "Hi, I am Ana. Please note that I am allergic to almonds." }] }'
{
"id": "gen_UZC1z4lf40BCLd2D",
"trace_id": "trace_2GMd3tLYNtTVJ4E1",
"status": "completed",
"output": {
"model": "glm-4.7-flash",
"content": "Hello Ana, I have noted that you are allergic to almonds. How can I assist you with your order today?",
"finish_reason": "stop"
}
}
The reply says "noted", but the agent itself wrote nothing. The rule reads this turn once it has completed.
5. See what it learned
GET /v1/projects/{project_id}/memories
lists one store's facts. Extraction runs after the turn, so if the list is
still empty, wait a few seconds and ask again.
- CLI
- SDK
- curl
naturali list-memories \
--project-id "$PROJECT" \
--memory-store-id "$STORE"
const { data: memories } = await naturali.memories.listMemories({
path: { project_id: process.env.PROJECT! },
query: { memory_store_id: process.env.STORE! },
});
curl "https://api.naturali.ai/v1/projects/$PROJECT/memories?memory_store_id=$STORE" \
-H "Authorization: Bearer $NATURALI_TOKEN"
{
"data": [
{
"id": "mem_3j1NM67SfBEEiruo",
"memory_store_id": "mstore_EisX1nAWTwPPwPBd",
"content": "Ana is allergic to almonds.",
"source_type": "manual",
"tags": {},
"invalidated_at": null,
"version": 1,
"created_at": "2026-10-03T08:40:40.450Z"
}
],
"total": 1,
"limit": 50,
"offset": 0
}
The extractor turned the message into one atomic fact. The same result is on
the turn itself:
GET /v1/projects/{project_id}/generations/{generation_id}
for step 4 carries extraction, keyed by rule id —
{ "candidates": 1, "created": 1, "skipped": 0, "superseded": 0 } here — and
memory_assertions, one row per write.
6. Ask in a new conversation
This request carries only the new message: nothing from step 4 is in it. The question does not mention almonds either, so an answer that does came from the store.
- CLI
- SDK
- curl
naturali create-agent-generation \
--project-id "$PROJECT" \
--agent-id "$AGENT" \
--wait true \
--messages '[{"role":"user","content":"This is Ana again. Is there anything I should avoid when I order?"}]'
const { data: recalled } = await naturali.agents.createAgentGeneration({
path: { project_id: process.env.PROJECT!, agent_id: process.env.AGENT! },
query: { wait: true },
body: {
messages: [
{
role: 'user',
content:
'This is Ana again. Is there anything I should avoid when I order?',
},
],
},
});
console.log(recalled?.output?.content);
curl -X POST \
"https://api.naturali.ai/v1/projects/$PROJECT/agents/$AGENT/generate?wait=true" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "messages": [{ "role": "user", "content": "This is Ana again. Is there anything I should avoid when I order?" }] }'
{
"id": "gen_3G5L6BI3tZXkivzs",
"trace_id": "trace_5DuYNDg9pbOjUuWa",
"status": "completed",
"output": {
"model": "glm-4.7-flash",
"content": "Yes, you should avoid ordering anything that contains almonds.",
"finish_reason": "stop"
}
}
That is the value: the allergy reached a conversation that never contained it.
The rule keeps reading every turn of this agent, the agent's own replies included, so the store grows with what the agent says as well as what it is told. A near-duplicate is skipped rather than stored twice; see What a write returns.
What's next
- Memories → Correct a fact — replace what the agent believes, keeping the history of why.
- Memory stores → One store or several — one store per customer, so one customer's facts never reach another's answers.
- Memory rules — a
promptthat narrows what counts as a fact, or your own agent or tool as the handler. - Answer from your documents — the same
knowledge_config, fed by documents you supply instead of facts the agent learns.