---
title: "Building memory UIs"
description: "Endpoints shaped for user-facing memory features — graph rendering, a memories feed, merge, and delete."
---

> Documentation Index
> Fetch the complete documentation index at: https://truememory-docs.mvstg.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Building memory UIs

These endpoints are shaped for building user-facing memory features: an interactive graph view, a "Memories" tab, and the management actions (merge duplicates, delete) that go with them.

## Memory Graph (Visualization)

Returns the full knowledge graph — entities (nodes) and their relationships (edges) — for a user. Designed for rendering an interactive graph in your frontend.

```bash
GET https://your-tie-host/memories/graph
Authorization: Bearer $TOKEN
```

| Parameter | In | Required | Description |
|-----------|------|----------|-------------|
| `agent_id` | query | No | Scope to a specific agent. Omit to get memories across all agents. |
| `graph_id` | query | No | Render a [graph namespace](/memory/graph-namespacing/) instead of the user's graph. Requires a service token. Mutually exclusive with `agent_id`; no subject user needed. |
| `limit` | query | No | Max edges per page (default 200, max 500) |
| `cursor` | query | No | Opaque keyset cursor from the previous page's `next_cursor`, passed back verbatim (`<iso-datetime>\|<edge-id>`). Malformed values return `400 VALIDATION_ERROR`. |

### Which entities appear

The response is one graph: the deduplicated entity nodes (`User` / `Person` / `Topic` / `HealthMetric` / `Document` / …) and the `RELATES_TO` facts between them. Every entity with a live fact in scope appears, whatever produced it — a chat exchange, an agent-tool write, or an API ingest of any `type`.

Two things never affect what is shown. `source_description` is a provenance label for your own debugging and record resolution; it does not classify episodes. And no entity type is filtered out, so a book, article, or report the user brought in shows as a `Document` node like any other entity.

Episodes themselves are not nodes in this response. They stay stored verbatim; `GET /memories/export?include_episodes=true` returns them. A UI that grouped nodes labelled `Note` (for example a "Notes" section) now finds that group empty — the notes' extracted entities appear under their own types instead.

**Response:**

```json
{
  "nodes": [
    {
      "id": "node-uuid-1",
      "name": "Fauzaan",
      "summary": "You work as a Staff Engineer at Mindvalley and have a cat named Lucy.",
      "summary_raw": "Fauzaan is a user.\nFauzaan was promoted to Staff Engineer at Mindvalley.\nFauzaan has a cat named Lucy.",
      "labels": ["User"],
      "attributes": {},
      "created_at": "2026-04-07T10:30:00+00:00"
    },
    {
      "id": "node-uuid-2",
      "name": "Mindvalley",
      "summary": "You were promoted to Staff Engineer at Mindvalley.",
      "summary_raw": "Mindvalley is the company where Fauzaan works.\nFauzaan was promoted to Staff Engineer at Mindvalley.",
      "labels": ["Topic"],
      "attributes": {},
      "created_at": "2026-04-07T10:30:00+00:00"
    }
  ],
  "edges": [
    {
      "id": "edge-uuid-1",
      "source": "node-uuid-1",
      "target": "node-uuid-2",
      "label": "works_at",
      "fact": "Fauzaan works at Mindvalley",
      "created_at": "2026-04-07T10:30:00+00:00",
      "valid_at": "2026-04-07T10:30:00+00:00"
    }
  ],
  "next_cursor": null
}
```

Each **node** is an entity (person, place, project, concept) with:
- `id` — unique identifier, use as the node key in your graph
- `name` — display label
- `summary` — display-ready prose. Humanized rewrite when the background humanizer has run for this node (addresses the user as "you/your" when relevant), with automatic fallback to the raw graphiti string otherwise. Good for tooltips, popovers, or the entity-detail panel.
- `summary_raw` — graphiti's untouched newline-joined edge facts (third-person, dossier-style). Useful for a "show original" toggle in the UI or for debugging extraction quality.
- `labels` — entity types like `User`, `Person`, `Topic`, `Goal`, `Insight`
- `attributes` — extracted structured attributes (varies per entity type; may be empty)
- `created_at` — when the entity was first written to the graph

Each **edge** is a fact connecting two nodes:
- `source` / `target` — node IDs this edge connects
- `label` — relationship type (e.g. `works_at`, `knows`, `prefers`)
- `fact` — the full natural-language statement (third-person; edge-fact humanization is on the roadmap)
- `created_at` / `valid_at` — when the fact was recorded / became true

**Pagination:** When `next_cursor` is not `null`, pass it as `?cursor={value}` to fetch the next page. Each page returns up to `limit` edges and all nodes referenced by those edges.

**Example with pagination:**

```bash
# First page
GET /memories/graph?limit=100

# Next page (using next_cursor from previous response)
GET /memories/graph?limit=100&cursor=2026-04-28T08:34:00+00:00|edge-uuid-100
```

**Example scoped to one agent:**

```bash
GET /memories/graph?agent_id=chatbot&limit=50
```

:::tip[Frontend integration]
The response shape maps directly to graph visualization libraries like [D3 Force Graph](https://d3js.org/d3-force), [vis.js](https://visjs.org/), or [React Flow](https://reactflow.dev/). Use `nodes` as vertices and `edges` with `source`/`target` as connections.
:::

## Memory Timeline

Recently active entities first — **one card per entity**, never per fact. Each item is an entity with its humanized summary as the body, and its connected edge facts inlined for an "N facts" disclosure. This is the right shape for any "Memories" tab or memory browser UI.

```bash
GET https://your-tie-host/memories/timeline
Authorization: Bearer $TOKEN
```

| Parameter | In | Required | Description |
|-----------|------|----------|-------------|
| `agent_id` | query | No | Scope to one agent. Omit for memories across all agents. |
| `graph_id` | query | No | Feed from a [graph namespace](/memory/graph-namespacing/) instead of the user's graph. Mutually exclusive with `agent_id`; no subject user needed. |
| `limit` | query | No | Items (entities) per page (default 20, max 200) |
| `cursor` | query | No | Opaque keyset cursor from the previous page's `next_cursor`, passed back verbatim (`<iso-datetime>\|<entity-id>`). Malformed values return `400 VALIDATION_ERROR`. |
| `sort` | query | No | `last_updated` (default) or `created` |

**Response:**

```json
{
  "items": [
    {
      "id": "node-uuid-1",
      "name": "Mindvalley",
      "labels": ["Topic"],
      "summary": "You were promoted to Staff Engineer at Mindvalley about five months ago and have been working on AI there.",
      "summary_raw": "Mindvalley is the company where Fauzaan works.\nFauzaan was promoted to Staff Engineer at Mindvalley.\nFauzaan works on AI at Mindvalley.",
      "first_seen_at": "2026-03-12T14:20:00+00:00",
      "last_updated_at": "2026-04-28T08:34:00+00:00",
      "fact_count": 3,
      "facts": [
        {
          "id": "edge-uuid-1",
          "label": "works_at",
          "fact": "Fauzaan works at Mindvalley",
          "created_at": "2026-04-28T08:34:00+00:00",
          "valid_at": "2026-04-28T08:34:00+00:00",
          "direction": "incoming",
          "other": {
            "id": "node-uuid-2",
            "name": "Fauzaan",
            "labels": ["User"]
          }
        }
      ]
    }
  ],
  "next_cursor": "2026-04-28T08:34:00+00:00|node-uuid-1",
  "sort": "last_updated"
}
```

Each item is one entity with:
- `id` / `name` / `labels` — entity identity (use `labels[0]` as the entity-type chip)
- `summary` — display-ready prose. Humanized rewrite when the background humanizer has run (addresses the user as "you/your" when the user appears in the entity's facts), with automatic fallback to the raw graphiti string otherwise. Render this as the card body.
- `summary_raw` — graphiti's untouched newline-joined edge facts. Useful for a "show original" toggle.
- `first_seen_at` — when this entity first entered the graph
- `last_updated_at` — `max(edge.created_at)` across the entity's live edges; the most recent activity touching this entity
- `fact_count` — number of live edges. Render as "N facts" before the disclosure expands.
- `facts[]` — each connected edge inlined with its other endpoint:
  - `label` — relationship type (e.g. `works_at`, `prefers`)
  - `fact` — full natural-language statement
  - `created_at` / `valid_at` — when the edge was written / is real-world-true
  - `direction` — `"outgoing"` if this entity is the source of the edge, `"incoming"` if the target. Helps render directional UIs ("you → works_at → Mindvalley" vs "Mindvalley ← works_at ← you").
  - `other` — the entity on the other side of the edge

Entities with zero live (non-expired) edges are excluded from the response — there's nothing to render.

**Pagination:** When `next_cursor` is not `null`, pass it as `?cursor={value}` to fetch the next page. Treat the value as **opaque** and send it back byte-for-byte — it carries both a timestamp and the last item's id, because timestamps are not unique (one ingest batch stamps many facts with the same second) and the id is what keeps tied items from being skipped. Pages never overlap and never drop items. `next_cursor` is `null` when you've reached the end; a page that happens to fill exactly still returns a cursor, so the final request may come back with an empty `items` array. Keep requesting until `next_cursor` is `null` rather than stopping on a short page.

There is no `offset` or `page` parameter — cursor is the only pagination mechanism, and unknown query parameters are ignored rather than rejected.

**Picking a `sort` mode:**

- **`last_updated`** (default) — entities ordered by their most recent fact. New conversations push entities to the top of the feed. Good for "what's been happening" UIs.
- **`created`** — entities ordered by when they first entered the graph. Stable order regardless of recent activity. Good for "show me the things I've told you about" archival views.

**Example: first page across all agents:**

```bash
GET /memories/timeline?limit=20
```

**Example: paginating:**

```bash
# Next page (using next_cursor from previous response)
GET /memories/timeline?limit=20&cursor=2026-04-28T08:34:00+00:00|node-uuid-20
```

**Example: entities in graph-creation order, scoped to one agent:**

```bash
GET /memories/timeline?agent_id=chatbot&sort=created&limit=50
```

:::tip[One card per entity, never per fact]
A common antipattern is to render each edge (each `RELATES_TO` row in Neo4j) as a separate memory card. That produces dozens of triple-fragment cards from a single conversation. The timeline endpoint deliberately groups by entity so the card count stays low and each card carries the full prose summary — render `summary` as the body, and put `facts[]` behind a "N facts" disclosure that expands inline.
:::

:::tip[Why not `/memories/{agent_id}`?]
The older `GET /memories/{agent_id}` endpoint (below) is relevance-ranked against a fixed prompt, capped at 50 results, and not paginated — fine for seeding agent context, wrong shape for a user-facing browser. `/memories/timeline` is what to use for any list-of-memories UI.
:::

## Merge Two Memories

Sometimes the graph ends up with two entity nodes that represent the same real-world thing — `Mohamed Fauzaan` and `Fauzaan`, or `Acme Inc.` and `Acme`. Merge collapses them into one: every live relationship that pointed at the duplicate is repointed at the primary, the LLM rewrites the primary's summary and attributes to combine both records, and the duplicate is hidden from search.

```bash
POST https://your-tie-host/memories/{agent_id}/merge
Authorization: Bearer $TOKEN
Content-Type: application/json
```

**Request body:**

```json
{
  "primary_id": "node-uuid-keep",
  "duplicate_id": "node-uuid-fold-in"
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `primary_id` | string | Yes | UUID of the entity node to keep. Its name, summary, attributes, and embedding are preserved. |
| `duplicate_id` | string | Yes | UUID of the entity node to fold into the primary. Must differ from `primary_id`. |
| `graph_id` | string | No | Merge within a [graph namespace](/memory/graph-namespacing/) instead of the user's memory. Service token required; `agent_id` is ignored. |

Both UUIDs come from the `id` field of nodes in [Memory Graph (Visualization)](#memory-graph-visualization).

**Response:**

```json
{
  "primary_id": "node-uuid-keep",
  "duplicate_id": "node-uuid-fold-in",
  "rewired_outgoing": 3,
  "rewired_incoming": 2,
  "audit_edge_id": "audit-uuid",
  "merged_at": "2026-05-04T10:30:00+00:00",
  "enriched": true,
  "enriched_summary": "Mohamed Fauzaan is a senior engineer at Mindvalley working on the Truememory platform. Previously known in the graph as 'Fauzaan'.",
  "enriched_labels": ["Person", "Engineer"]
}
```

- `rewired_outgoing` / `rewired_incoming` — counts of live edges repointed from the duplicate to the primary.
- `audit_edge_id` — UUID of the `IS_DUPLICATE_OF` edge written from duplicate → primary for traceability.
- `merged_at` — timestamp the merge committed.
- `enriched` — `true` when the LLM successfully rewrote the primary's summary and attributes; `false` if the LLM call failed and the merge fell back to a blind rewire.
- `enriched_summary` / `enriched_labels` — the new summary and unioned labels written onto the primary. `null` when `enriched` is `false`.

**What changes after a merge:**

- Every live edge incident to the duplicate is replaced with an equivalent edge incident to the primary. The original edge is soft-deleted (its timeline is preserved for undo).
- The duplicate node is marked with `merged_into` and disappears from List Memories, Search, and the Memory Graph response.
- The primary node's `summary`, `attributes`, and `labels` are **rewritten by the LLM** to combine both records into one canonical view. Its `name` and `name_embedding` are kept as-is — pick the more specific or canonical node as `primary_id`. Order matters.

:::note[The merge always calls an LLM]
Smart enrichment runs on every merge, using the same model Truememory configures for entity extraction. Expect the call to add **1–3 seconds** of latency on top of the rewire. If the model errors or returns malformed output, the merge falls back to a blind rewire and `enriched` is set to `false` — the user-confirmed merge is never blocked by a model outage.
:::

:::caution[Edge fact text is not rewritten]
If the original edge text said "Fauzaan likes tea", the `fact` field still literally says "Fauzaan likes tea" after the merge — only the endpoint pointer changed. The Memory Graph response will show the enriched primary node alongside the old fact text. If you need the fact text rewritten, delete the edge and re-ingest a corrected statement.
:::

:::note[Duplicate facts are not deduped]
If both nodes had similar relationships (e.g. both said "likes coffee"), you will end up with two live edges. The next memory-ingestion pass usually collapses them, or you can delete one manually with [Delete a Memory](#delete-a-memory).
:::

**Constraints:**

- Both nodes must live in the same scope — same user, same `agent_id`. Cross-agent merges are rejected.

**Error cases:**

| Status | Code | When |
|--------|------|------|
| `400` | `MEMORY_MERGE_INVALID` | `primary_id` equals `duplicate_id`, or one of the UUIDs is not in the requested scope |
| `501` | `SERVICE_UNAVAILABLE` | The memory backend is disabled for this deployment |

## Delete a Memory

:::caution[Deprecated route — see Deleting data]
This route deletes one edge but its path doesn't say so. Use [`DELETE /memories/{agent_id}/edges/{edge_id}`](/memory/deleting-data/#delete-an-edge) in new code — same behavior, explicit name. For topic-level deletion (a node and all its facts in one call) see [Delete a node](/memory/deleting-data/#delete-a-node). This route keeps serving unchanged until existing callers migrate.
:::

```bash
DELETE https://your-tie-host/memories/{agent_id}/{memory_id}
Authorization: Bearer $TOKEN
```

| Parameter | In | Required | Description |
|-----------|------|----------|-------------|
| `graph_id` | query | No | Delete from a [graph namespace](/memory/graph-namespacing/) instead of the user's memory. Service token required; `agent_id` is ignored. |

**Response:**

```json
{
  "status": "deleted",
  "memory_id": "edge-abc123"
}
```

Returns `404` if the memory is not found.

:::note
In the List Memories response, `source` indicates the graph element type (always `"edge"`, representing a relationship between entities). `memory_type` is always `"long_term"` for knowledge graph memories.
:::

## List Memories (deprecated)

:::caution[Use `/memories/timeline` instead]
This endpoint is **deprecated** and will be removed in a future release. It is relevance-ranked against a fixed prompt (not time-ordered), hard-capped at 50 results, has no pagination, and pays an LLM embed + vector + reranker cost on every call. For user-facing memory lists, use [`GET /memories/timeline`](#memory-timeline) — it returns flat, time-ordered, cursor-paginated facts with selectable `sort`.
:::

```bash
GET https://your-tie-host/memories/{agent_id}
Authorization: Bearer $TOKEN
```

**Response:**

```json
[
  {
    "id": "edge-abc123",
    "title": "Works on Truememory Auth",
    "name": "Truememory Auth integration",
    "content": "User is integrating Truememory Auth with Finerminds",
    "memory_type": "long_term",
    "source": "edge"
  }
]
```

Source: https://truememory-docs.mvstg.com/memory/building-uis/index.mdx
