---
title: Give an agent a memory
description: "relate, the embedded Cypher graph database (relatedb.ai): Build a memory store an agent can keep across sessions, write episodes from the CLI and over MCP, then recall facts by meaning with their evidence."
type: tutorial
surfaces:
  - cli
  - mcp
  - cypher
  - agents
status: stable
verified: 0.1.2
verified_by:
  - cypher
  - cli
canonical_url: https://relatedb.ai/docs/get-started/agent-memory/
prerequisites:
  - get-started/install
related:
  - guides/agents/model-agent-memory
  - cookbook/agent-memory
  - cookbook/bitemporal-decisions
  - guides/agents/connect-over-mcp
  - guides/agents/drive-relate-from-an-agent
  - concepts/problems-relate-solves#agent-memory
last_updated: 2026-11-09
---

# Give an agent a memory

Build a memory store an agent can keep across sessions, write episodes from the CLI and over MCP, then recall facts by meaning with their evidence.

> Verified against relate 0.1.2. If your `relate --version` is newer, read https://relatedb.ai/docs/reference/changelog/#since-0.1.2

In about ten minutes, we give an agent a memory that outlives its session: one file, `memory.rl`, holding what it learned, where it learned it, and which later fact corrected an earlier one. We write two episodes from the command line and a third over MCP, the way an agent host does, then recall by meaning. The data model comes from [Model agent memory](https://relatedb.ai/docs/guides/agents/model-agent-memory/), which explains each label and property; this page builds it and uses it.

## Prerequisites

- `relate` on your `PATH`: `relate --version` prints `Relate v0.1.2` on its first line. The [install page](https://relatedb.ai/docs/get-started/install/) takes about two minutes.
- A terminal on macOS or Linux, in an empty directory.

## Create the memory store

A memory attaches to the things it is about. Ours is about the shared example graph, three people and two projects, and every person and project becomes an `Entity` with a lowercase `key`. The schema adds a [constraint](https://relatedb.ai/docs/reference/glossary/#constraint) that keeps one node per key, episode and fact, an index on the fact's slot, and a vector index for recall by meaning. Write it all to a script:

```bash verify
cat > memory.cypher <<'EOF'
CREATE (ada:Person {name: 'Ada', role: 'engineer'}),
       (grace:Person {name: 'Grace', role: 'engineer'}),
       (noor:Person {name: 'Noor', role: 'maintainer'}),
       (relate:Project {name: 'relate', language: 'Rust'}),
       (tck:Project {name: 'opencypher-tck', language: 'Gherkin'}),
       (ada)-[:WORKS_ON {since: 2024}]->(relate),
       (grace)-[:WORKS_ON {since: 2023}]->(relate),
       (grace)-[:WORKS_ON {since: 2022}]->(tck),
       (noor)-[:MAINTAINS]->(relate),
       (ada)-[:KNOWS]->(grace);
MATCH (n) WHERE n:Person OR n:Project SET n:Entity, n.key = toLower(n.name);
CREATE CONSTRAINT entity_key IF NOT EXISTS FOR (e:Entity) REQUIRE e.key IS UNIQUE;
CREATE CONSTRAINT episode_id IF NOT EXISTS FOR (ep:Episode) REQUIRE ep.id IS UNIQUE;
CREATE CONSTRAINT fact_id IF NOT EXISTS FOR (f:Fact) REQUIRE f.id IS UNIQUE;
CREATE INDEX fact_slot IF NOT EXISTS FOR (f:Fact) ON (f.slot);
CREATE VECTOR INDEX fact_embedding IF NOT EXISTS
  FOR (f:Fact) ON (f.embedding) WITH [f.closedAt]
  OPTIONS {indexConfig: {`vector.dimensions`: 4, `vector.similarity_function`: 'cosine'}};
MATCH (e:Entity) RETURN e.key AS key ORDER BY key;
EOF
```

Run it into a new [store](https://relatedb.ai/docs/reference/glossary/#store), `memory.rl`, which relate creates because nothing is there yet:

```bash verify
relate query --database ./memory.rl --mode table < memory.cypher
```

```text
+------------------+
| key              |
+------------------+
| "ada"            |
| "grace"          |
| "noor"           |
| "opencypher-tck" |
| "relate"         |
+------------------+
```

Five entities, each with the key a memory write matches on. The vector index takes 4-dimension vectors so this page's results never change; a real embedding model's vectors have hundreds of dimensions.

## Remember a session from the CLI

An episode is one thing the agent saw: a message, or a tool call and its result. From each one, the agent's model extracts a fact. One statement writes a batch of episodes, each with its fact: the fact comes `FROM` its episode and is `ABOUT` the entities it names. When a new fact answers the same slot as an older current one, the statement closes the older one and links them with `SUPERSEDES`, so nothing is deleted. Save it:

```bash verify
cat > remember.cypher <<'EOF'
UNWIND $episodes AS m
CREATE (ep:Episode {id: m.id, session: m.session, kind: m.kind, at: datetime(m.at), text: m.text})
CREATE (f:Fact {id: m.factId, text: m.fact, slot: m.slot, validFrom: datetime(m.validFrom),
                recordedAt: datetime(m.at), embedding: vector(m.embedding, 4, FLOAT32)})
CREATE (f)-[:FROM]->(ep)
WITH ep, f, m
CALL (ep, f, m) {
  UNWIND m.about AS key
  MATCH (n:Entity {key: key})
  CREATE (ep)-[:MENTIONS]->(n), (f)-[:ABOUT]->(n)
}
CALL (f) {
  MATCH (old:Fact {slot: f.slot}) WHERE old.recordedAt < f.recordedAt AND old.closedAt IS NULL
  SET old.validTo = f.validFrom, old.closedAt = f.recordedAt
  CREATE (f)-[:SUPERSEDES]->(old)
  RETURN count(old) AS replaced
}
RETURN ep.id AS episode, f.text AS fact, replaced;
EOF
```

In session `s1`, the agent hears two messages. This file is what the extraction produced for them, embeddings included:

```bash verify
cat > session1.json <<'EOF'
[
  {"id": "e1", "session": "s1", "kind": "message", "at": "2026-03-02T10:00:00Z",
   "text": "Grace does the release review for relate.", "about": ["grace", "relate"],
   "factId": "f1", "fact": "Grace reviews each relate release.", "slot": "relate/release-reviewer",
   "validFrom": "2026-01-05T00:00:00Z", "embedding": [0.9, 0.1, 0.0, 0.2]},
  {"id": "e2", "session": "s1", "kind": "message", "at": "2026-03-02T10:05:00Z",
   "text": "Noor goes through new relate issues every Monday.", "about": ["noor", "relate"],
   "factId": "f2", "fact": "Noor triages new relate issues on Mondays.", "slot": "relate/triage",
   "validFrom": "2026-02-02T00:00:00Z", "embedding": [0.1, 0.9, 0.2, 0.0]}
]
EOF
```

`--fetch-params episodes=session1.json` reads the file and binds it as `$episodes`. Write the session:

```bash verify
relate query --database ./memory.rl --mode table --fetch-params episodes=session1.json < remember.cypher
```

```text
+---------+----------------------------------------------+----------+
| episode | fact                                         | replaced |
+---------+----------------------------------------------+----------+
| "e1"    | "Grace reviews each relate release."         | 0        |
| "e2"    | "Noor triages new relate issues on Mondays." | 0        |
+---------+----------------------------------------------+----------+
```

Two episodes, two facts, and nothing replaced: the memory had nothing to correct yet.

## Remember a tool call over MCP

An agent host talks to relate over [MCP](https://relatedb.ai/docs/reference/glossary/#mcp) (the Model Context Protocol): it starts `relate serve --mcp stdio` and sends JSON-RPC messages, one per line, on its stdin. In session `s2`, a tool call reads the repository's owners file and finds that Ada took over release review on 10 March. These three lines are what a host sends: `initialize`, the `notifications/initialized` notification, then a `write_cypher` call with the same statement and the new episode as its `parameters`:

```bash verify
cat > requests.jsonl <<'EOF'
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"shell","version":"0"}}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"write_cypher","arguments":{"statement":"UNWIND $episodes AS m CREATE (ep:Episode {id: m.id, session: m.session, kind: m.kind, at: datetime(m.at), text: m.text}) CREATE (f:Fact {id: m.factId, text: m.fact, slot: m.slot, validFrom: datetime(m.validFrom), recordedAt: datetime(m.at), embedding: vector(m.embedding, 4, FLOAT32)}) CREATE (f)-[:FROM]->(ep) WITH ep, f, m CALL (ep, f, m) { UNWIND m.about AS key MATCH (n:Entity {key: key}) CREATE (ep)-[:MENTIONS]->(n), (f)-[:ABOUT]->(n) } CALL (f) { MATCH (old:Fact {slot: f.slot}) WHERE old.recordedAt < f.recordedAt AND old.closedAt IS NULL SET old.validTo = f.validFrom, old.closedAt = f.recordedAt CREATE (f)-[:SUPERSEDES]->(old) RETURN count(old) AS replaced } RETURN ep.id AS episode, f.text AS fact, replaced","parameters":{"episodes":[{"id":"e3","session":"s2","kind":"tool_call","at":"2026-03-20T09:00:00Z","text":"read_file CODEOWNERS: release/ @ada","about":["ada","relate"],"factId":"f3","fact":"Ada reviews each relate release.","slot":"relate/release-reviewer","validFrom":"2026-03-10T00:00:00Z","embedding":[0.85,0.15,0.05,0.25]}]}}}}
EOF
```

Now play the host. This server opens the store writable on purpose, because this step writes; a server that only recalls takes `--read-only`, as in [Connect over MCP](https://relatedb.ai/docs/guides/agents/connect-over-mcp/). The replies go to `replies.jsonl`, and relate's own status lines to the terminal:

```bash verify
relate serve --mcp stdio --database ./memory.rl < requests.jsonl > replies.jsonl
```

```text
serve: MCP (stdio) serving on stdin/stdout (auth: none)
serve: stdin closed, shutting down
```

The server answered each request and stopped when its input ended, as it does when a host closes it. `replies.jsonl` holds two lines, since a notification gets no reply. The first names the server, `relate-mcp` 0.1.2. The second is the write's result as JSON text inside the reply: `write_cypher` returns counters, not rows, so it shows `"nodes_created":2` and `"relationships_created":6`, an empty `rows`, and timings that change on every run. Let's read the store to see what the write did:

```bash verify
relate --database ./memory.rl --read-only --mode table "MATCH (f:Fact)-[:FROM]->(ep:Episode) RETURN ep.id AS episode, ep.kind AS kind, f.text AS fact, f.closedAt IS NULL AS current ORDER BY episode"
```

```text
+---------+-------------+----------------------------------------------+---------+
| episode | kind        | fact                                         | current |
+---------+-------------+----------------------------------------------+---------+
| "e1"    | "message"   | "Grace reviews each relate release."         | false   |
| "e2"    | "message"   | "Noor triages new relate issues on Mondays." | true    |
| "e3"    | "tool_call" | "Ada reviews each relate release."           | true    |
+---------+-------------+----------------------------------------------+---------+
```

Notice that Grace's fact is no longer current, and it is still there. The tool call corrected it, and the memory kept both.

## Recall by meaning

A recall takes the embedding of a question, finds the nearest facts that are still current, and brings back the evidence for each and what it replaced. The cells in this section run in your browser, on an in-memory store with the same engine: the first two hold the same memory as `memory.rl`, written in one batch, and the third is the recall.

```cypher run name=memory
CREATE (ada:Person {name: 'Ada', role: 'engineer'}),
       (grace:Person {name: 'Grace', role: 'engineer'}),
       (noor:Person {name: 'Noor', role: 'maintainer'}),
       (relate:Project {name: 'relate', language: 'Rust'}),
       (tck:Project {name: 'opencypher-tck', language: 'Gherkin'}),
       (ada)-[:WORKS_ON {since: 2024}]->(relate),
       (grace)-[:WORKS_ON {since: 2023}]->(relate),
       (grace)-[:WORKS_ON {since: 2022}]->(tck),
       (noor)-[:MAINTAINS]->(relate),
       (ada)-[:KNOWS]->(grace);
MATCH (n) WHERE n:Person OR n:Project SET n:Entity, n.key = toLower(n.name);
CREATE CONSTRAINT entity_key IF NOT EXISTS FOR (e:Entity) REQUIRE e.key IS UNIQUE;
CREATE CONSTRAINT episode_id IF NOT EXISTS FOR (ep:Episode) REQUIRE ep.id IS UNIQUE;
CREATE CONSTRAINT fact_id IF NOT EXISTS FOR (f:Fact) REQUIRE f.id IS UNIQUE;
CREATE INDEX fact_slot IF NOT EXISTS FOR (f:Fact) ON (f.slot);
CREATE VECTOR INDEX fact_embedding IF NOT EXISTS
  FOR (f:Fact) ON (f.embedding) WITH [f.closedAt]
  OPTIONS {indexConfig: {`vector.dimensions`: 4, `vector.similarity_function`: 'cosine'}};
MATCH (e:Entity) RETURN e.key AS key ORDER BY key
```

```table
| key              |
|------------------|
| "ada"            |
| "grace"          |
| "noor"           |
| "opencypher-tck" |
| "relate"         |
5 rows · nodes created: 5, relationships created: 5, properties set: 18, labels added: 10, indexes added: 2, constraints added: 3
```

```json data=episodes
{"episodes": [
  {"id": "e1", "session": "s1", "kind": "message", "at": "2026-03-02T10:00:00Z",
   "text": "Grace does the release review for relate.", "about": ["grace", "relate"],
   "factId": "f1", "fact": "Grace reviews each relate release.", "slot": "relate/release-reviewer",
   "validFrom": "2026-01-05T00:00:00Z", "embedding": [0.9, 0.1, 0.0, 0.2]},
  {"id": "e2", "session": "s1", "kind": "message", "at": "2026-03-02T10:05:00Z",
   "text": "Noor goes through new relate issues every Monday.", "about": ["noor", "relate"],
   "factId": "f2", "fact": "Noor triages new relate issues on Mondays.", "slot": "relate/triage",
   "validFrom": "2026-02-02T00:00:00Z", "embedding": [0.1, 0.9, 0.2, 0.0]},
  {"id": "e3", "session": "s2", "kind": "tool_call", "at": "2026-03-20T09:00:00Z",
   "text": "read_file CODEOWNERS: release/ @ada", "about": ["ada", "relate"],
   "factId": "f3", "fact": "Ada reviews each relate release.", "slot": "relate/release-reviewer",
   "validFrom": "2026-03-10T00:00:00Z", "embedding": [0.85, 0.15, 0.05, 0.25]}
]}
```

```cypher run name=remember params=episodes
UNWIND $episodes AS m
CREATE (ep:Episode {id: m.id, session: m.session, kind: m.kind, at: datetime(m.at), text: m.text})
CREATE (f:Fact {id: m.factId, text: m.fact, slot: m.slot, validFrom: datetime(m.validFrom),
                recordedAt: datetime(m.at), embedding: vector(m.embedding, 4, FLOAT32)})
CREATE (f)-[:FROM]->(ep)
WITH ep, f, m
CALL (ep, f, m) {
  UNWIND m.about AS key
  MATCH (n:Entity {key: key})
  CREATE (ep)-[:MENTIONS]->(n), (f)-[:ABOUT]->(n)
}
CALL (f) {
  MATCH (old:Fact {slot: f.slot}) WHERE old.recordedAt < f.recordedAt AND old.closedAt IS NULL
  SET old.validTo = f.validFrom, old.closedAt = f.recordedAt
  CREATE (f)-[:SUPERSEDES]->(old)
  RETURN count(old) AS replaced
}
RETURN ep.id AS episode, f.text AS fact, replaced
```

```table
| episode | fact                                         | replaced |
|---------|----------------------------------------------|----------|
| "e1"    | "Grace reviews each relate release."         | 0        |
| "e2"    | "Noor triages new relate issues on Mondays." | 0        |
| "e3"    | "Ada reviews each relate release."           | 1        |
3 rows · nodes created: 6, relationships created: 16, properties set: 35, labels added: 6
```

The question is "Who reviews relate releases?", and this is its embedding:

```json data=question
{"vector": [0.88, 0.12, 0.02, 0.22], "k": 2}
```

```cypher run name=recall params=question
MATCH (f:Fact)
SEARCH f IN (VECTOR INDEX fact_embedding FOR $vector WHERE f.closedAt IS NULL LIMIT $k) SCORE AS score
MATCH (f)-[:FROM]->(ep:Episode)
RETURN f.text AS memory, round(score, 4) AS score, ep.text AS evidence,
       COLLECT { MATCH (f)-[:SUPERSEDES]->(old:Fact) RETURN old.text } AS replaced
ORDER BY score DESC
```

```table
| memory                                       | score  | evidence                                            | replaced                               |
|----------------------------------------------|--------|-----------------------------------------------------|----------------------------------------|
| "Ada reviews each relate release."           | 0.999  | "read_file CODEOWNERS: release/ @ada"               | ["Grace reviews each relate release."] |
| "Noor triages new relate issues on Mondays." | 0.6178 | "Noor goes through new relate issues every Monday." | []                                     |
2 rows
```

Grace's fact is the nearest by meaning, and it does not come back: the `WHERE` inside the search drops closed facts before the `LIMIT` counts. Ada's fact answers, with the tool call it rests on and the fact it replaced.

## Replay it as an agent's calls

An agent does the same work through MCP tools instead of a shell. The trace below replays this page's writes and reads as the `tools/call` requests an agent host sends to `relate serve`, on the store the `memory` cell builds: session `s1`'s two episodes, the tool call from session `s2`, the read-back, and the recall. Each step runs in your browser when we step to it, and shows what `relate serve`'s tool returns for that call, built from the engine's result. The trace writes the same episodes as the `remember` cell, so when the cells have already run, its first step empties the page's database before it starts, and after its last step it empties the database again, so the cells run on what they write.

```json data=memory-trace src=https://relatedb.ai/docs/get-started/agent-memory/data/memory-trace.json
{
  "data_block": "memory-trace",
  "inline": false,
  "url": "https://relatedb.ai/docs/get-started/agent-memory/data/memory-trace.json",
  "media_type": "application/json",
  "bytes": 3881,
  "sha256": "d5a0d455211636083ff5341ec8951c54377c4fce60e058b1b8ea84bf86b3271f",
  "shape": {"steps":"array of 4 objects"},
  "first": {"steps":{"tool":"write_cypher","note":"Session `s1`'s two episodes, with the statement and file this page writes them with from the CLI (`remember.cypher`, `session1.json`), sent as one `write_cypher` call.","arguments":{"statement":"UNWIND $episodes AS m CREATE (ep:Episode {id: m.id, session: m.session, kind: m.kind, at: datetime(m.at), text: m.text}) CREATE (f:Fact {id: m.factId, text: m.fact, slot: m.slot, validFrom: datetime(m.validFrom), recordedAt: datetime(m.at), embedding: vector(m.embedding, 4, FLOAT32)}) CREATE (f)-[:FROM]->(ep) WITH ep, f, m CALL (ep, f, m) { UNWIND m.about AS key MATCH (n:Entity {key: key}) CREATE (ep)-[:MENTIONS]->(n), (f)-[:ABOUT]->(n) } CALL (f) { MATCH (old:Fact {slot: f.slot}) WHERE old.recordedAt < f.recordedAt AND old.closedAt IS NULL SET old.validTo = f.validFrom, old.closedAt = f.recordedAt CREATE (f)-[:SUPERSEDES]->(old) RETURN count(old) AS replaced } RETURN ep.id AS episode, f.text AS fact, replaced","parameters":{"episodes":"array of 2 objects"}}}}
}
```

The trace starts on the database the cells up to `memory` build. Each step below ran when the page was recorded, one after another on that database, through the mapping the MCP tryout uses; each result is what relate serve's tool returns for that call, built from the engine's result. No agent and no server took part in the recording.

**Step 1 of 4: `write_cypher`.** Session `s1`'s two episodes, with the statement and file this page writes them with from the CLI (`remember.cypher`, `session1.json`), sent as one `write_cypher` call.

```json
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"write_cypher","arguments":{"statement":"UNWIND $episodes AS m CREATE (ep:Episode {id: m.id, session: m.session, kind: m.kind, at: datetime(m.at), text: m.text}) CREATE (f:Fact {id: m.factId, text: m.fact, slot: m.slot, validFrom: datetime(m.validFrom), recordedAt: datetime(m.at), embedding: vector(m.embedding, 4, FLOAT32)}) CREATE (f)-[:FROM]->(ep) WITH ep, f, m CALL (ep, f, m) { UNWIND m.about AS key MATCH (n:Entity {key: key}) CREATE (ep)-[:MENTIONS]->(n), (f)-[:ABOUT]->(n) } CALL (f) { MATCH (old:Fact {slot: f.slot}) WHERE old.recordedAt < f.recordedAt AND old.closedAt IS NULL SET old.validTo = f.validFrom, old.closedAt = f.recordedAt CREATE (f)-[:SUPERSEDES]->(old) RETURN count(old) AS replaced } RETURN ep.id AS episode, f.text AS fact, replaced","parameters":{"episodes":[{"id":"e1","session":"s1","kind":"message","at":"2026-03-02T10:00:00Z","text":"Grace does the release review for relate.","about":["grace","relate"],"factId":"f1","fact":"Grace reviews each relate release.","slot":"relate/release-reviewer","validFrom":"2026-01-05T00:00:00Z","embedding":[0.9,0.1,0.0,0.2]},{"id":"e2","session":"s1","kind":"message","at":"2026-03-02T10:05:00Z","text":"Noor goes through new relate issues every Monday.","about":["noor","relate"],"factId":"f2","fact":"Noor triages new relate issues on Mondays.","slot":"relate/triage","validFrom":"2026-02-02T00:00:00Z","embedding":[0.1,0.9,0.2,0.0]}]}}}}
```

Counters: `nodes_created` 4, `relationships_created` 10, `properties_set` 22, `labels_added` 4; `rows` is empty, as `write_cypher` returns counters.

**Step 2 of 4: `write_cypher`.** Session `s2`'s tool call, the `write_cypher` request in `requests.jsonl` that this page sends to the server over stdio.

```json
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"write_cypher","arguments":{"statement":"UNWIND $episodes AS m CREATE (ep:Episode {id: m.id, session: m.session, kind: m.kind, at: datetime(m.at), text: m.text}) CREATE (f:Fact {id: m.factId, text: m.fact, slot: m.slot, validFrom: datetime(m.validFrom), recordedAt: datetime(m.at), embedding: vector(m.embedding, 4, FLOAT32)}) CREATE (f)-[:FROM]->(ep) WITH ep, f, m CALL (ep, f, m) { UNWIND m.about AS key MATCH (n:Entity {key: key}) CREATE (ep)-[:MENTIONS]->(n), (f)-[:ABOUT]->(n) } CALL (f) { MATCH (old:Fact {slot: f.slot}) WHERE old.recordedAt < f.recordedAt AND old.closedAt IS NULL SET old.validTo = f.validFrom, old.closedAt = f.recordedAt CREATE (f)-[:SUPERSEDES]->(old) RETURN count(old) AS replaced } RETURN ep.id AS episode, f.text AS fact, replaced","parameters":{"episodes":[{"id":"e3","session":"s2","kind":"tool_call","at":"2026-03-20T09:00:00Z","text":"read_file CODEOWNERS: release/ @ada","about":["ada","relate"],"factId":"f3","fact":"Ada reviews each relate release.","slot":"relate/release-reviewer","validFrom":"2026-03-10T00:00:00Z","embedding":[0.85,0.15,0.05,0.25]}]}}}}
```

Counters: `nodes_created` 2, `relationships_created` 6, `properties_set` 13, `labels_added` 2; `rows` is empty, as `write_cypher` returns counters.

**Step 3 of 4: `read_cypher`.** The read this page runs with the CLI after that write: every fact with its episode, and whether it is still current.

```json
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"read_cypher","arguments":{"statement":"MATCH (f:Fact)-[:FROM]->(ep:Episode) RETURN ep.id AS episode, ep.kind AS kind, f.text AS fact, f.closedAt IS NULL AS current ORDER BY episode"}}}
```

| episode | kind      | fact                                       | current |
| ------- | --------- | ------------------------------------------ | ------- |
| e1      | message   | Grace reviews each relate release.         | false   |
| e2      | message   | Noor triages new relate issues on Mondays. | true    |
| e3      | tool_call | Ada reviews each relate release.           | true    |

**Step 4 of 4: `read_cypher`.** The recall, the statement and question of the `recall` cell above and of `recall.cypher`.

```json
{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"read_cypher","arguments":{"statement":"MATCH (f:Fact)\nSEARCH f IN (VECTOR INDEX fact_embedding FOR $vector WHERE f.closedAt IS NULL LIMIT $k) SCORE AS score\nMATCH (f)-[:FROM]->(ep:Episode)\nRETURN f.text AS memory, round(score, 4) AS score, ep.text AS evidence,\n       COLLECT { MATCH (f)-[:SUPERSEDES]->(old:Fact) RETURN old.text } AS replaced\nORDER BY score DESC","parameters":{"vector":[0.88,0.12,0.02,0.22],"k":2}}}}
```

| memory                                     | score  | evidence                                          | replaced                                |
| ------------------------------------------ | ------ | ------------------------------------------------- | --------------------------------------- |
| Ada reviews each relate release.           | 0.999  | read_file CODEOWNERS: release/ @ada               | \["Grace reviews each relate release."] |
| Noor triages new relate issues on Mondays. | 0.6178 | Noor goes through new relate issues every Monday. | \[]                                     |

The rows match cell `recall`'s recording.

Live: Step sends the next call and Play all sends the rest, each to the page's database in your browser on the store the cells build up to the trace's starting cell, and each step shows the result `relate serve`'s tool returns for that call, built from the engine's live result; a trace that would write twice starts on an emptied database, and the database is emptied again when the trace ends, so the page's cells run on what they write.

The last step is the recall cell's statement and question, sent as `read_cypher`, and its rows are the cell's: the memory the trace's calls wrote answers the same way as the one the cells wrote.

## Recall from the terminal

The same recall runs against `memory.rl`. Save it as a script:

```bash verify
cat > recall.cypher <<'EOF'
MATCH (f:Fact)
SEARCH f IN (VECTOR INDEX fact_embedding FOR $vector WHERE f.closedAt IS NULL LIMIT $k) SCORE AS score
MATCH (f)-[:FROM]->(ep:Episode)
RETURN f.text AS memory, round(score, 4) AS score, ep.text AS evidence,
       COLLECT { MATCH (f)-[:SUPERSEDES]->(old:Fact) RETURN old.text } AS replaced
ORDER BY score DESC;
EOF
```

A person reads a table; an agent parses the [envelope](https://relatedb.ai/docs/reference/glossary/#envelope), one JSON object per result, and branches on the [exit code](https://relatedb.ai/docs/reference/glossary/#exit-code). Both open the store `--read-only`, so a recall can never change the memory.

### At a terminal

```bash verify
relate query --database ./memory.rl --read-only --mode table --param 'vector=[0.88,0.12,0.02,0.22]' --param k=2 < recall.cypher
```

```text
+----------------------------------------------+--------+-----------------------------------------------------+----------------------------------------+
| memory                                       | score  | evidence                                            | replaced                               |
+----------------------------------------------+--------+-----------------------------------------------------+----------------------------------------+
| "Ada reviews each relate release."           | 0.999  | "read_file CODEOWNERS: release/ @ada"               | ["Grace reviews each relate release."] |
| "Noor triages new relate issues on Mondays." | 0.6178 | "Noor goes through new relate issues every Monday." | []                                     |
+----------------------------------------------+--------+-----------------------------------------------------+----------------------------------------+
```

The same two memories as the cell, read from the file.

### From an agent

```bash verify
relate query --database ./memory.rl --read-only --mode envelope --param 'vector=[0.88,0.12,0.02,0.22]' --param k=2 < recall.cypher
```

```text
{"columnTypes":["STRING","FLOAT","STRING","LIST"],"columns":["memory","score","evidence","replaced"],"notifications":[],"planCache":"miss","rows":[{"evidence":"read_file CODEOWNERS: release/ @ada","memory":"Ada reviews each relate release.","replaced":["Grace reviews each relate release."],"score":0.999},{"evidence":"Noor goes through new relate issues every Monday.","memory":"Noor triages new relate issues on Mondays.","replaced":[],"score":0.6178}],"timings":{"elapsed_ms":0.654417,"execute_ms":0.407083,"lex_parse_ms":0.048834,"lower_ms":0.06183400000000001,"parse_ms":0.1575,"plan_ms":0.0795,"result_generation_ms":0.00025,"rewrite_ms":0.009041},"truncated":false}
```

A question embedded with the wrong model has the wrong length. The statement fails before it runs: the message goes to stderr, the error object to stdout, and the exit code is 6:

```bash verify
relate query --database ./memory.rl --read-only --mode envelope --param 'vector=[0.88,0.12,0.02]' --param k=2 < recall.cypher
echo "exit=$?"
```

```text
type error: VectorIndexDimensionalityMismatch
{"error":{"code":"validation_error","exit_code":6,"kind":"type","message":"type error: VectorIndexDimensionalityMismatch","retryable":false}}
exit=6
```

`retryable` is `false`: the same call fails again until the question is embedded with the model the facts were.

Notice that both views name the tool call as the evidence. An agent can quote it instead of asserting the fact on its own.

## What you built

You have a memory in one file, `memory.rl`: entities, three episodes, and three facts, one of which a later tool call corrected without erasing. It took writes from the command line and from an MCP client with the same statement, and it answers a question by meaning with the evidence attached. The agent's next session starts with an empty prompt and remembers all three facts.

## Next

- [Long-term memory for an agent](https://relatedb.ai/cookbook/agent-memory/): two sessions of extractions, decisions, and supersession on a larger memory.
- [Bi-temporal decisions](https://relatedb.ai/cookbook/bitemporal-decisions/): what was true on a date, and what the agent believed on that date, through late corrections.
- [Model agent memory](https://relatedb.ai/docs/guides/agents/model-agent-memory/): each label, property and time axis this page used, and how to ask what the agent believed on a past date.
- [Install a task skill](https://relatedb.ai/docs/guides/agents/install-the-skill/#install-a-task-skill): the `relate-agent-memory` skill, which gives an agent this model and its statements as reference text, at `https://relatedb.ai/skills/relate-agent-memory/SKILL.md`.

## Terms used on this page

- **constraint**: A rule the store enforces on every write, such as a unique property, a required property, a property type or a node key; a violating write fails.
- **store**: A relate database: a directory, a single `.rl` file, or `:memory:`, opened with `--database`.
- **MCP**: The Model Context Protocol, which agent hosts use to call tools; `relate serve --mcp` exposes a store as `read_cypher`, `get_schema` and `write_cypher`.
- **envelope**: One compact JSON object per result set, printed by `--mode envelope`, that keeps the column list even when there are no rows.
- **exit code**: The number a `relate` run returns to the shell; each nonzero value names a class of failure, such as 2 for usage, 5 for conflict, 6 for a rejected statement.
