Tutorial

Give your n8n AI agent a memory that survives sessions

An n8n AI Agent with the standard memory nodes remembers a conversation. It does not remember a customer. Close the chat, come back tomorrow, and the agent greets the same person from zero — every preference, every past issue, gone. This guide covers why that happens and how to fix it properly with a self-hosted memory layer on Postgres, using the n8n-nodes-haki community node.

Last updated: August 2026. Tested on n8n 1.x, self-hosted and Cloud.

Why your agent forgets (it's usually one of three things)

Before adding anything, check which of these you're actually hitting — the first one accounts for most "my agent has amnesia" threads on the forum:

1. Your sessionId isn't stable. The built-in memory nodes key everything on the session ID. If your workflow generates a new one per execution (a webhook execution ID, a timestamp), every message starts a fresh conversation. The memory works; the key doesn't. Fix: derive the session from something stable — the WhatsApp number, the email, the user ID.

2. The context window ends. Buffer-style memory keeps the last N turns of one session. That's execution context, and trimming it is correct — the mistake is expecting it to also be long-term knowledge. It can't be: it's a sliding window, and yesterday slid out.

3. The key/value workaround destroys history. The common forum answer — a Postgres table of user_id / key / value — works until a value changes. An UPDATE silently erases what the user used to want (and you'll want that history the first time you debug a weird answer). An INSERT leaves two contradicting rows and no way to arbitrate. What's missing is lifecycle: which value is current, which was replaced, and when.

Long-term memory is a different job from session memory: extract durable facts from conversations, keep them updated when they change, and inject the relevant ones at the start of any future conversation — whatever the session ID.

What we're building

Webhook  →  Haki Context  →  AI Agent (+ its normal session memory)  →  Haki Capture  →  Respond

Two nodes do the work. Haki Context runs before the agent: it fetches this user's current facts (dated, with replaced values excluded) and hands them to the agent as a system-prompt block. Haki Capture runs after: it records the exchange so tonight's consolidation can extract anything durable. The agent's own buffer memory stays — short-term and long-term are complementary, not rivals.

The memory itself runs on your infrastructure: Haki is Apache 2.0, one docker compose up, Postgres 16 + pgvector, embeddings computed locally. If you self-host n8n for data reasons, the same reasons apply double to memory — this setup keeps the whole loop on your machines.

Setup

1. Run Haki (5 minutes)

git clone https://github.com/GetHaki/Haki && cd Haki
docker compose up -d
uv sync && uv run alembic upgrade head
uv run uvicorn app.main:app --port 8100

Then prove the memory loop before wiring anything into n8n:

uv run haki connect --api-url http://localhost:8100
uv run haki verify

You want the last line to read OK — your agent remembered across conversations, and it can prove it. (Alternatively: the hosted version at gethaki.space gives you 1,000 credits/month, no card — the n8n side is identical, only the base URL changes.)

Create an API key:

curl -X POST http://localhost:8100/v1/keys \
  -d '{"org_id":"org_main","project_id":"prj_support","label":"n8n"}'

2. Install the community node (2 minutes)

n8n → Settings → Community Nodes → Install → enter n8n-nodes-haki → confirm. Two nodes appear in your palette: Haki Context and Haki Capture. (Self-hosted n8n installs community nodes directly; on n8n Cloud, available once the node is listed as verified.)

3. Create the credential (1 minute)

First use of either node → Create new credential: your API base URL (http://your-host:8100 — from n8n-in-Docker, that's your host's address, not localhost) and the hk_... key. The credential is stored by n8n; it never appears in the workflow JSON.

4. Wire the workflow

On Haki Context, three fields matter:

FieldSet it toWhy
Subject IDan expression resolving to a stable user identifier ({{ $json.from }} for a WhatsApp number, an email, a CRM ID)this is the identity memory attaches to — the whole point
Project IDprj_supportkeys and isolation are per-project
Querythe incoming user messageretrieval is relevance-ranked; the message is the query

Feed the node's output into the agent's system prompt, above your own instructions — it renders as a compact block of dated facts, with anything superseded already excluded.

Haki Capture, after the agent: same Subject ID and Project, plus the user message and the agent's reply. Capture is idempotent — an n8n retry won't create duplicate memories, which matters more than it sounds once webhooks start double-firing.

A note the node enforces rather than suggests: a call without a stable subject identity is refused. Memory attached to nothing is how you end up serving one customer's facts to another.

5. Test the thing that matters: a NEW session

First conversation:

You: Hi, I'm Amara. Quick thing — always reply to me in French please, and I'm on the Pro plan. Agent: (answers, in French)

Now the real test. New chat, new session ID — simulating tomorrow:

You: What plan am I on again? Agent: Vous êtes sur le plan Pro. (in French, unprompted)

Two facts survived a session boundary: the plan, and the language preference — applied, not just recited. Then change your mind (« actually, English is fine ») and ask again in a third session: the agent should switch, and if you open the Haki console you'll find the French preference still there, marked superseded, with the date. That's the difference between memory and a k/v table: the history exists, but it can never be served as current.

Download: the complete workflow JSON — haki-persistent-support-agent.json — is in the repo under integrations/n8n/, importable into any n8n instance. It uses plain HTTP nodes, so it also works without installing the community node.

Troubleshooting

SymptomCauseFix
Agent remembers nothing across sessionsSubject ID unstable (execution ID, timestamp)key on the user, not the run — check what the expression actually resolves to in past executions
Memory works in test, dies in productionwebhook payload shape differs → Subject ID resolves empty → call refusedinspect the refused-call error; it names the missing field
Facts appear but stale ones tooyou're injecting raw history alongside — the buffer window re-serves what memory correctly retiredkeep the buffer short (last few turns); durable knowledge comes from Context, not the transcript
ECONNREFUSED from the noden8n runs in Docker, localhost points at the n8n containeruse the host address or a shared Docker network
Slow first call after startuplocal embedding model loadingone-time cost; subsequent calls are fast (p95 under 50 ms at 10k facts in our benchmark)

Where this goes next

The same memory follows the same user across surfaces: the MCP server puts it in Cursor or Claude, the OpenAI-compatible gateway adds it to any existing chat app by changing one base URL, and the SDKs cover coded agents. One user, one subject ID, one memory — n8n is just the first place it pays off.