---
title: "A support agent that remembers every customer: a blueprint"
slug: build-customer-support-memory
date: 2026-10-11
author: Semurg
section: how-to
tags: [build-on-semurg, customer-support, agents, memory, blueprint]
summary: A blueprint for a support desk whose agents forget customers between conversations: customers and tickets as a graph, every conversation stored with its date, and the history in front of the agent first.
status: published
covers: [route:POST /v1/memory/remember, route:POST /v1/memory/recall, route:POST /v1/memory/context, route:POST /v1/memory/forget, sdk:context, sdk:remember]
---
A customer writes in for the third time about the same problem, and the agent answering has never heard of them. The history exists,
scattered across a ticket system, a chat log and an email thread, but nothing hands it to the agent at the moment it starts typing.
This blueprint gives a support agent, human-assisted or fully automated, a memory of each customer that outlives the conversation and
the channel.

> Diagram desk-graph: Priya Raman works at Northwind Retail and opened the invoice export ticket, which is about the invoice export; each conversation is a dated passage that mentions her.

Everything below uses only what the published release does.

## The shape of the data

- **One scope per support desk.** A scope is a separate memory; one desk's customers are invisible to another's. Agents on the same desk share one memory, so what one agent learned yesterday is in front of the next one today.
- **Entities**: each customer, their company, each ticket, each product or feature they ask about.
- **Relationships**: a customer `WORKS_AT` a company, `OPENED` a ticket; a ticket is `ABOUT` a feature.
- **Conversations** are stored as text with their date. The node cuts the text into passages and links each one to the customers and features it names, so a later question about the customer brings back what was said, and when.

## Step one: the customer and their ticket

When a ticket is opened, your helpdesk integration writes what it already knows as facts:

```bash runnable kind=shell-node needs=token
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/memory/remember \
  -H "Authorization: Bearer $SEMURG_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"scope":"support-desk","date":"2026-10-02","source":"helpdesk: new ticket",
       "facts":{"entities":[{"name":"Priya Raman","type":"Customer"},{"name":"Northwind Retail","type":"Company"},
                            {"name":"Invoice export ticket","type":"Ticket"},
                            {"name":"Invoice export","type":"Feature"}],
                "relations":[{"source":"Priya Raman","target":"Northwind Retail","type":"WORKS_AT"},
                             {"source":"Priya Raman","target":"Invoice export ticket","type":"OPENED"},
                             {"source":"Invoice export ticket","target":"Invoice export","type":"ABOUT"}]}}'
```

```expect
expect: a JSON answer with "ok": true
assert: json:.ok==true
```

## Step two: every conversation, as it ends

At the end of each conversation, send what was said as text, with its date. No model is needed for this: the node stores the text
now, cuts it into passages and links each passage to every entity it already knows that the text names.

```bash runnable kind=shell-node needs=token
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/memory/remember \
  -H "Authorization: Bearer $SEMURG_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"scope":"support-desk","date":"2026-10-09","source":"chat transcript",
       "content":"Priya Raman wrote again about the Invoice export. The CSV file still drops the currency column for orders in euros. She asked to be contacted by email rather than phone."}'
```

```expect
expect: a JSON answer with "ok": true; the reply also says how many passages no stored entity reaches
assert: json:.ok==true
```

The reply tells you how many passages reached no stored entity. A passage that names nobody the desk knows is stored, but nothing
will lead back to it, so write the customer and the topic as facts first, as in step one.

## Step three: the history, before the agent answers

When Priya writes in again, the agent asks the memory first. Context takes the question, starts from every stored name in it, walks
out from them and returns the facts and the conversations that mention them, each with its date:

```bash runnable kind=shell-node needs=token
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/memory/context \
  -H "Authorization: Bearer $SEMURG_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"scope":"support-desk","query":"What has Priya Raman contacted us about?","hops":2}'
```

```expect
expect: "state": "answered", with the invoice export ticket and the conversation about the missing currency column
assert: json:.state=="answered" and ([..|strings|ascii_downcase|select(contains("currency column"))]|length>0)
```

An agent often has only a first name. Recall finds an entity by one whole word of a longer name, so "Priya" reaches Priya Raman:

```bash runnable kind=shell-node needs=token
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/memory/recall \
  -H "Authorization: Bearer $SEMURG_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"scope":"support-desk","subject":"Priya"}'
```

```expect
expect: "state": "answered", with Priya Raman's company and her invoice export ticket
assert: json:.ok==true and .state=="answered" and ([..|strings|ascii_downcase|select(contains("invoice export"))]|length>0)
```

If two customers share a first name, recall can reach either of them: check the facts that come back before you rely on them, and
use the full name when you have it.

## The agent loop, in Python

The same two calls frame every conversation: read the history before the first reply, write the conversation when it ends. With the
Python client from the client archive at `https://one.semurg.io/dl/clients.tar.gz` (unpack it, then `pip install -e clients/python`):

> Diagram support-loop: A customer writes in; the agent asks the desk's memory for the history first and answers with it in front of it; when the conversation ends it is written back with its date, linked to the customer, ready for next time.

```python
import datetime
import os
from semurg import Client

DESK = "support-desk"

def history_for(memory, customer, message):
    # before the agent's first reply: what the desk already knows about this customer and this topic
    answer = memory.context(DESK, customer + ": " + message, hops=2)
    if answer.get("state") != "answered":
        return []
    return [(item.get("source_date"), item.get("text")) for item in answer.get("items", [])]

def conversation_ended(memory, transcript):
    # after the conversation: store it with today's date; it links itself to the customers and topics it names
    today = datetime.date.today().isoformat()
    return memory.remember(DESK, content=transcript, date=today, source="chat transcript")

with Client("http://localhost:4000", token=os.environ["SEMURG_API_TOKEN"]) as memory:
    for when, text in history_for(memory, "Priya Raman", "the export is still broken"):
        print(when, text)
```

Hand the agent the history as context in its prompt, and tell it to say so when the history is empty rather than guess.

## Connecting an agent framework

If your support agent is built on an agent framework, it can call the memory itself over MCP, with the desk's scope in the
`X-Semurg-Scope` header. The [agents page](/agents) has a complete program for each framework we test, with the release it last ran on.

## When a customer asks to be forgotten

Forgetting an entity deletes it, its facts and the passages linked to it; a passage that contains the name without being linked to it
(for example text stored before the entity was) stays, and the reply lists it in `passages_retained`, so forget that document too if it
must go. What is deleted is unreachable at once, and its bytes are wiped from the disk later, with space reclaim, which is not released
yet.

> Diagram what-forget-removes: Forgetting a customer deletes their entity, every fact that touches it and the conversations linked to them, all unreachable at once; a conversation that only contains the name is kept and listed in the reply; the dead records stay on disk until space reclaim, which is not released yet.

```bash
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/memory/forget \
  -H "Authorization: Bearer $SEMURG_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"scope":"support-desk","subject":"Priya Raman","reason":"customer request"}'
```

Be clear about what that is on today's release, because a support desk has obligations here:

- It is not yet erasure from the disk. The customer, their facts and the conversations linked to them are unreachable at once, but the deleted records stay on disk as dead records until space reclaim, which is not released yet; nothing is rewritten in place.
- A conversation that contains the name without being linked to the customer, for example one stored before you wrote the customer as a fact, is kept: the reply lists it in `passages_retained`. Forget that document too if it must go.
- The name must match exactly. A close spelling is refused with up to three suggestions and is never forgotten by accident.
- Forgetting the same customer again answers `200` with `already_forgotten: true`, and a later write that names the customer brings them back.

If your obligations require erasure of every copy, this release does not do that for you yet.

## Scale and cost

A single node is free, with no licence key, and it is the full engine. A cluster is licensed per node; talk to us about the one you need. We have not measured this workload,
so this post states no speed for it.

## What it does not do today

- It does not find new names in a conversation. Text sent as content is linked to customers and topics the desk already knows; a new customer or topic has to be written as a fact first, unless an extraction model is installed and serving on the node, which then reads the stored text afterwards (the reply's `model_extraction` says which). How quickly it does that has not been measured from outside yet.
- It matches meaning only with the node's embedding model installed: until then, a customer who writes about "the spreadsheet download" is not matched to "Invoice export". A node that agrees to downloads installs the model once the signed catalogue lists it.
- It does not detect, mask or classify personal information.

## When something is refused

- `401`: the token is missing or wrong.
- `422` `missing 'scope'`: every HTTP memory call needs a scope in its body.
- `content` and `passages` cannot travel in one call: send them in two.
- A forget of a name that does not exist exactly answers `404`, with up to three close names to check.
- `"state": "nothing_stored"` is an answer, not an error: nothing in the desk's memory matched.

## Next

- [Give your agent persistent memory with MCP in five minutes](/blog/agent-memory-mcp-five-minutes): the node and the seven memory tools.
- [A matter memory for a legal team](/blog/build-legal-matter-memory): the same pattern for parties and clauses.
