---
title: Giving Your Agent Memory
tag: giving-your-agent-memory
order: 3
summary: >-
  Use the workspace memory store: recall facts into any AI node, write memories
  from conversations and documents, and operate on them directly.
---

# Giving Your Agent Memory

The agent from the first two guides keeps a transcript: one chat, replayed from the top each turn,
growing until it compacts. Start a new chat and it begins from nothing.

Memory is a different thing entirely. It is a store in your workspace, not a property of a
conversation, and giving your agent access to it is two nodes.

## What you will build

```text
Telegram Message ─> Recall Memory ──memory──> AI Conversation        (part one, two nodes)

Cron Trigger ─> Table ─> JavaScript ─> AI Agent ─> JavaScript ─> Memory (insert)
                                          ^
                          Structured Output Schema                  (part two)
```

## Memory in Verdalia

A memory is a short piece of text stored in your workspace together with an embedding of it. The
embedding is what makes retrieval semantic: a query returns memories that are close to it in
meaning rather than memories containing the same words. Asking "when should I schedule focus time?"
finds *Freja protects 09:00 to 12:00 for deep work* without the word "focus" appearing in it.

Stored beside the text:

- the **namespace** — which corpus the memory belongs to
- `properties` — free-form metadata you can filter on
- `initial_relevancy` — how good the signal was when it was written, from an authoritative
  observation down to a weak inference
- `half_life_seconds` — how durable it is; relevancy decays over that half-life into
  `current_relevancy`
- `links` — where it came from, and how it relates to other memories

There are three things you do with the store, and everything in this guide is one of them.

**Break a source into memories and store them.** Anything that contains facts can become memories:
a chat log run through a model, a document, a meeting transcript, a news report. The **Memory** node
writes them, either from text it chunks itself or from facts you hand it.

**Recall against a query and give the result to a model.** The **Recall Memory** node takes a query,
returns the memories closest to it, and feeds the `memory` socket on **AI Agent** or **AI
Conversation**. Any AI node in any workflow, not just a chat agent.

**Operate on memories directly.** The same **Memory** node does `search`, `get`, `list`, `update`,
`delete`, and link management with no model involved. Memory is workspace data, and an ordinary
workflow can read and write it like any other data.

## Prerequisites

Memory needs an embedding model, separate from the model your agent talks with. Add an **OpenAI**
connection under **Settings**, then **Connections**, and use `text-embedding-3-small`.

Memories live in a namespace, and a new workspace has none. Open the **Memory** page, choose
**Namespaces**, type `assistant` under **New namespace**, and **Create** it.

**Note**: every node that touches memory selects its own embedding model, and they must all match.
The provider, model, and dimensions are recorded on each memory when it is written, and text
embedded with one model cannot be searched with another. Pick one and use it everywhere.

Searching the node palette for "memory" also turns up **Memory Cache Store** and **Memory Cache Get
Or Compute**. Those are a short-lived key-value cache for workflow runs and are unrelated.

## Part one: recall in your agent

**1. Add recall.** Add a **Recall Memory** node. Connect the **Telegram Message** node to its
`input` socket, and set:

- **query** to `{{ input.text }}` — the default, and the message you just sent
- **Namespaces** to your `assistant` namespace
- **model** to your embedding model

If you followed the second guide, the message text may come from a transcribed voice note. Wire the
node that produces the joined text into `input` and set the query to `{{ input }}`.

**2. Wire it into the agent.** Connect Recall Memory's `output` socket to the **AI Conversation**
node's `memory` socket.

That is the whole change. Recalled memories are formatted into context for that turn only. They
never enter the transcript, so they cost nothing on later turns and cannot pollute the stored
conversation.

Nothing will come back yet, because the store is empty. The rest of the guide is about filling it.

## Part two: writing memories

Every writer has the same shape: take a source, turn it into facts, insert them.

### Letting the agent write its own

The quickest route is to give the agent the tools and let it decide.

**3. Add memory tools.** Add a **Memory Tools** node, set **Namespace** to `assistant`, select the
same embedding model, and turn on `search` and `insert`. Connect its `tools` output to the same
**Flatten** node feeding the agent's `tools` socket.

Each enabled operation becomes one tool the model can call: `memory_search`, `memory_insert`,
`memory_get`, `memory_update`, `memory_delete`, `memory_link_add`, `memory_link_remove`. They are
individual toggles, so search without write is a valid configuration, and a good default — the
agent can look things up on demand while something more deliberate decides what gets kept.

Set **tool_name** to give the tools a prefix, which matters when one agent has two memory tool
nodes pointing at different namespaces.

A tool call cannot change the namespace, model, or connection. Those come from the node.

### Distilling conversations on a schedule

Nothing has to write memories during a conversation, and a scheduled job reading the day's
transcripts produces better ones. This is also the general pattern for turning any source into
memories.

**4. Trigger it nightly.** Add a **Cron Trigger** node. On the **Simple** tab set **Run workflow**
to *Once per day*, **Hour** to 3, and **Minute** to 0, then pick the node's **Timezone**. The
**Advanced** tab takes a raw cron expression if you need one.

**5. Load the conversations.** Add a **Table** node pointed at `Agent Conversation Data`, operation
Query, **Dynamic Filters** off with no filters configured, and a limit high enough to cover your
chats. Connect the Cron Trigger to it.

This is the one case where you want an unfiltered query: the job reads every chat rather than one.

**6. Take only what is new.** Add a **JavaScript** node connected to the Table node:

```javascript
const rows = inputs.input;
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
const lines = [];

for (const row of rows) {
  for (const entry of row.transcript) {
    const at = Date.parse(entry.timestamp);

    if (!Number.isFinite(at) || at < cutoff) {
      continue;
    }

    if (entry.kind.type === "user_message") {
      const text = entry.kind.content
        .filter(item => item.type === "text")
        .map(item => item.text)
        .join("\n");
      if (text.trim() !== "") lines.push(`user: ${text}`);
    }

    if (entry.kind.type === "assistant_message") {
      lines.push(`assistant: ${entry.kind.text}`);
    }
  }
}

return { count: lines.length, transcript: lines.join("\n") };
```

Only `user_message` and `assistant_message` entries are read. Reasoning, tool calls, and tool
results are the agent's working notes rather than things it learned.

**7. Describe what a memory looks like.** Add a **Structured Output Schema** node, which forces the
model to return parseable JSON instead of prose:

```json
{
  "type": "object",
  "additionalProperties": false,
  "required": ["memories"],
  "properties": {
    "memories": {
      "type": "array",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["text", "category", "importance"],
        "properties": {
          "text": {
            "type": "string",
            "description": "One durable fact, written so it stands alone."
          },
          "category": { "type": "string" },
          "importance": { "type": "string", "enum": ["low", "medium", "high"] }
        }
      }
    }
  }
}
```

**8. Extract the facts.** Add an **AI Agent** node. Connect the JavaScript node to its `input`
socket and the schema node to its `schema` socket. Set `max_turns` to 1 and the temperature low,
around 0.2.

**AI Agent** is the other AI node in the palette, and it suits this job better than **AI
Conversation**. Conversation takes a transcript and returns a transcript; Agent takes one input, a
templated prompt, and returns a `response` — or, with a schema wired in, a parsed object on
`structured`. Extraction is one shot, not a conversation.

Its `system_prompt` is where the quality lives:

```md
You are the nightly memory organizer.

Read the transcript and extract only durable facts that will still be useful in a month.

Keep: preferences, plans, decisions, recurring problems, project context,
relationships, deadlines, and stable personal context.

Discard: greetings, small talk, transient status, vague intentions, anything
you inferred rather than read, and anything already obvious.

Write each memory as a standalone sentence that makes sense without the transcript.
```

And `user_prompt` passes the transcript through:

```text
Conversations from the last day ({{ input.count }} messages):

{{ input.transcript }}
```

**9. Shape them for storage.** Add a **JavaScript** node fed by the agent's `structured` output:

```javascript
return inputs.input.memories.map(memory => ({
  text: memory.text,
  initial_relevancy: memory.importance === "high" ? 0.95
    : memory.importance === "medium" ? 0.85
    : 0.7,
  half_life_seconds: 31536000,
  properties: {
    category: memory.category,
    importance: memory.importance,
    source: "nightly-distillation"
  }
}));
```

Importance becomes starting relevancy, so a passing remark fades within the year while a firm
decision is still there. A half-life of `31536000` seconds is one year.

**10. Store them.** Add a **Memory** node, set the operation to **Insert**, pick your namespace,
and select your embedding model. Connect the previous node to it. It embeds each memory and writes
them into the namespace you picked.

**Note**: bulk insert takes a bare array of memory objects. A wrapper such as
`{ "memories": [...] }` is rejected. The objects carry no namespace of their own — one Insert node
writes one namespace, chosen in its Namespace field.

Run it manually once with a day of conversation behind you, then open **Memory** and read what it
decided to keep. Adjust the system prompt until you agree with it.

A refinement worth adding once it works: put a **Recall Memory** node in front of the extractor,
querying the same namespace, and wire it into the AI Agent's `memory` socket. The model then sees
what is already stored and stops proposing facts you have. That is the same recall node from part
one, in a workflow with no chat in it.

## Ingesting documents

A long document does not need a model to become memories. The **Memory** node's **Ingest**
operation chunks the text, embeds each chunk, and writes one memory per chunk:

```json
{
  "text": "Long document text...",
  "properties": { "kind": "reference" },
  "initial_relevancy": 0.9,
  "half_life_seconds": 31536000,
  "links": [
    { "relation": "source", "source": { "kind": "canopy_document", "value": "01J..." } }
  ]
}
```

Chunk size, overlap, and sentence-aware splitting are node config, as is the namespace — an Ingest
payload carries no namespace of its own either. `{ "documents": [...] }` ingests several at once.

This is how a reference file, an article, or a meeting transcript becomes something the agent can
quote. Distillation and ingestion are the same access point with different granularity: ingest
keeps the source's own words, distillation keeps only the conclusions.

**Prepare Memory** does the chunking and embedding without writing anything, for when you want to
route or filter chunks before they are stored.

## Using memory without an agent

Set the **Memory** node's operation to **Search** and it becomes an ordinary query node. It takes a
namespace and a text query and returns hits with their scores:

```json
{
  "namespace_id": "01J00000000000000000000001",
  "text": "invoices",
  "limit": 10,
  "min_semantic_score": 0.3,
  "min_current_relevancy": 0.5
}
```

Each hit is `{ memory, semantic_score, current_relevancy }`. `list` filters by namespace,
properties, or link without a query at all; `get` reads one memory by id; `update` and `delete` do
what they say.

This is what makes memory more than an agent feature. A morning brief can recall what you decided
yesterday, a triage job can check whether an issue is one you have seen before, and a report can
pull the facts it needs, none of them involving a chat.

## Corrections and provenance

Every link is `{ relation, source }`, and there are three relations:

- `source` — where the memory came from: a document, a table row, a URL, or another memory. Never
  followed, only recorded.
- `related` — an undirected association with another memory.
- `supersedes` — this memory replaces an older one.

Insert and Ingest accept `source` links only, because the other two name a memory that does not
exist yet at write time. Add those afterwards from the Memory page or with the agent's link tools;
the Memory node's own operation list does not offer them.

`supersedes` is the one worth reaching for. Superseding replaces a memory rather than deleting it:
the older one disappears from search and from recall, so a query returns the corrected version
rather than both. It stays readable through `get` and `list`, and removing the link makes it
active again.

A memory holds at most 32 links, `related` and `supersedes` must stay inside one namespace, and
supersession is linear — one predecessor, one successor.

## Tuning recall

Recall Memory's defaults are usually right, and worth knowing when they are not:

- `final_limit` — how many memories reach the model (12)
- `min_semantic_score` — how closely a memory must match the query (0.3)
- `min_current_relevancy` — how faded a memory may be and still count (0.5)
- `max_context_chars` — a ceiling on how much text is handed over (12000)

The middle two are independent gates and easy to confuse. One asks whether the memory is about what
you asked; the other asks whether the memory is still worth anything after decay. A memory has to
clear both.

If the agent brings up irrelevant things, raise `min_semantic_score` first. Recall returns direct
hits only: each hit carries its links as metadata, but a memory linked to a hit is not pulled in
with it.

## Namespaces must match

A namespace is a thing you make, not a word you type. Create one from **Namespaces** on the
**Memory** page, and every node that reads or writes memory picks it from a list. Nothing resolves
a namespace by its name, so you can rename one whenever it stops describing what is in it and every
workflow pointing at it carries on unchanged.

That removes the failure this section used to be about — a memory written to `telegram-assistant`
sitting invisible to a recall over `assistant`, with neither node reporting a problem. What is left
is the ordinary kind of mismatch: a recall pointed at the wrong corpus. If recall comes back empty
when you expected results, open the **Memory** page, filter by the namespace you meant, and check
the memories are actually in it.

Separate namespaces for conversation facts, ingested documents, and project notes let one workflow
read a narrow slice while the agent reads several at once. Deleting one is refused while it still
holds memories, so a corpus cannot vanish from under a workflow by accident.

## Seeing what is stored

The **Memory** page in the sidebar lists every memory with its namespace, properties, and current
relevancy after decay.

Add a **Knowledge Graph** page to see them as a graph instead, with the links between memories and
back to their sources. A memory distilled from a document keeps a `source` link to it, so you can
follow any fact back to where it came from.

Switch the page to **3D** to see the same memories as a star map arranged by meaning: memories about
similar things sit together and are coloured by cluster, and links are drawn between them. Documents
join the map by meaning once **Document Embeddings** is on under **Assistant Preferences**; until
then they sit beside the memories they are linked to.

## Where to go next

The three access points compose in more ways than this guide uses. A workflow that ingests every
new document into its own namespace gives the agent a library to quote from. A weekly job that
searches for contradictions and adds `supersedes` links removes stale facts without deleting them.
A report can recall against a question nobody typed into a chat.
