Module 3: Memory: The Agent That Remembers
4. Persistent memory: session ID and storage
Description
By the end of this lesson you'll be able to configure a persistent memory node — Postgres Chat Memory or Redis Chat Memory — so a real customer's conversation survives an n8n restart and the passing of days, and you'll be able to decide what identity to use to separate that memory so two different customers never end up reading each other's history.
This matters for a very concrete reason: at some point, someone from support or compliance is going to ask you "show me exactly what the bot told this customer three months ago." With memory in RAM, that question has no answer — the history no longer exists. With persistent memory it does, and it's also a SQL query, not a manual reconstruction. That difference — being able to audit a past conversation — is what separates a prototype from a system you can put into production and defend in front of a client or a regulator.
Connection to the module: in lesson 3 you saw how the window of recent turns within a single session works — contextWindowLength, its size limits, when it's enough. This lesson doesn't repeat that: the two nodes you're going to use here share that exact same parameter, so you already know how to read it. What is new is what lesson 1 of this module called scope and storage: where the identity that groups the history comes from, and where — outside n8n's process — it gets saved. You also won't see yet how the agent chains several active turns within an ongoing conversation or how it resolves references like "the previous order" — that's specifically lesson 5's job, right after this one.
Whose memory it is, and where it lives
Think about the difference between a visitor badge and an employee ID. Reception prints a new visitor badge every time you enter the building: a new number, valid only for that day, useless if you come back the following week. An employee ID is different — it's the same card today, tomorrow, and next month, and it always opens the same personnel file no matter which door of the building you walk in through. The session memory you saw in lesson 3 works like the visitor badge: the identity that groups the history (the sessionId the chat widget generates) is new every time a tab is opened. What this lesson adds is the employee ID: an identity you choose, stable, that stays the same no matter when or from where that customer comes back.
In n8n, that choice literally lives in a selector inside the memory node, called Session ID, with two real options:
- Connected Chat Trigger Node (
fromInput, the default option): the node looks for asessionIdfield coming from a directly connected Chat Trigger. It's the visitor badge — useful, but ephemeral. - Define below (
customKey): you write an expression or a fixed value in the Key field. This is where you put a real customer identity — a phone number, an account ID — to make it the employee ID.
The part that tends to surprise people: if you leave the default option (fromInput) on a workflow that does not start with a Chat Trigger — for example, a Webhook that receives messages from your company's ticketing system — there's no sessionId to read. n8n doesn't make one up or fail silently: the node throws a real execution error, No session ID found, with this exact description: "Expected to find the session ID in an input field called 'sessionId' (this is what the chat trigger node outputs). To use something else, change the 'Session ID' parameter." That message is, basically, n8n telling you: "you chose the visitor-badge option, but there's no reception desk printing one here — switch to Define below."
The other half of the decision is where the history lives once it's grouped by that identity. Postgres Chat Memory and Redis Chat Memory are two nodes that replace n8n's process RAM with a real external store: a Postgres table or a Redis database, depending on which one you connect. Both expose the same ai_memory output you already used with Simple Memory, so from the AI Agent node's perspective nothing changes about how they connect — what changes completely is how well what they store survives.
Worked example
Go back to Configuration C you saw in the module's introduction: the TuTienda support agent, with sessionKey = {{ $json.customerPhone }} and Postgres Chat Memory. There was a detail left unexplained there — where customerPhone comes from — and now you know: this workflow doesn't start with the Chat Trigger you used in Module 1 (that node delivers an ephemeral sessionId, not anyone's phone number). It starts with a Webhook node, which TuTienda's ticketing system sends every message to, already carrying the authenticated customer's phone number in the request body:
# Payload arriving at the Webhook when a customer writes
{
"customerPhone": "5215512345678",
"message": "Where's my order #4521?"
}
And here's how the Postgres Chat Memory node connected to ai_memory ends up configured:
# Node: Postgres Chat Memory
credential = my Postgres credential (host, database, user, password)
sessionIdType = "Define below" # customKey — no Chat Trigger connected
sessionKey = "{{ $json.customerPhone }}" # Key: stable, the same today and in a month
tableName = "n8n_chat_histories" # default value — creates itself if it doesn't exist
contextWindowLength = 10
Monday's turn. The customer at phone number 5215512345678 asks about order #4521; the agent responds with the delivery date, as you already saw in lesson 1. Postgres Chat Memory saves that exchange in the n8n_chat_histories table, under session_id = "5215512345678".
A turn one week later. The same customer, same phone, writes: "I need the invoice for that order." A week passed between one turn and the other, and probably at least one n8n restart or redeploy. It doesn't matter: when this message arrives, n8n evaluates {{ $json.customerPhone }} again, gets the same session_id, and Postgres Chat Memory loads the saved history. The agent resolves that "that order" is #4521 without the customer having to repeat it.
The same day, a different customer. Another customer, phone number 5213398765432, writes for the first time asking about their order #7790. Their message arrives with a different customerPhone, so Postgres Chat Memory saves it under a different session_id — a new row, with no trace of the previous customer's history.
What to expect. You can confirm all three turns with a direct query against the table the node itself created:
# Query in Postgres — confirm persistence and separation by customer
SELECT session_id, message->>'type' AS role, message->>'content' AS content
FROM n8n_chat_histories
ORDER BY session_id, id;
session_id | role | content
------------------+-------+---------------------------------------------
5213398765432 | human | Where's my order #7790?
5213398765432 | ai | Your order #7790 is in transit...
5215512345678 | human | Where's my order #4521?
5215512345678 | ai | Your order arrives July 24.
5215512345678 | human | I need the invoice for that order.
5215512345678 | ai | Sure, I'll generate the invoice for order #4521...
Interpretation: two columns tell the whole story. session_id proves scope — every phone number has its own rows, they never mix — and the fact that the following week's turn shows up in the same table, with nobody having typed it in by hand again, proves storage — it survived the passage of time, and any restart that happened in between, because it never depended on n8n's process staying alive.
Postgres or Redis: how long you keep it, and who needs to query it
The Session ID mechanics are identical in both nodes — the fromInput/customKey selector and the Key field are exactly the same. What changes is the store underneath, and that choice does depend on the use case:
- Postgres Chat Memory saves every row forever, unless you delete it. It's the right option when you need a permanent, SQL-auditable history — like the TuTienda case, where support or compliance might need to review a conversation from months ago. The table has no built-in automatic cleanup mechanism.
- Redis Chat Memory adds a parameter Postgres doesn't have: Session Time To Live (
sessionTTL, in seconds). With a value greater than zero, the entire session expires on its own after that many seconds of inactivity. The default value is0— "never expires," the same permanent behavior as Postgres, but running on an in-memory database built for fast reads and writes, not for being queried with SQL afterward.
An example where Redis wins: an agent that answers questions about a seasonal promotion lasting six weeks. You don't need to save those conversations forever — in fact, you probably prefer they don't stay there indefinitely for data-retention reasons. You set sessionTTL = 5184000 (sixty days in seconds) and Redis deletes on its own the history of any customer who hasn't written back in those two months.
Common mistakes
Leaving "Session ID" at its default value when the workflow's trigger isn't a Chat Trigger, or when it is but you need real customer identity (conceptual). What happens: you connect Postgres Chat Memory or Redis Chat Memory, don't touch the Session ID selector — it stays on Connected Chat Trigger Node — and the workflow fails on the first execution with the No session ID found error, or (if the trigger is indeed a Chat Trigger) it works, but memory stays just as ephemeral as Simple Memory, because the sessionId the Chat Trigger provides is the same visitor badge from lesson 3. Why it happens: switching from Simple Memory to a persistent node solves storage automatically, but scope is still a manual decision — the selector doesn't change just because the node did. How to spot it: check the Session ID value on the node; if it says Connected Chat Trigger Node and your trigger is a Webhook, you're going to see the error on the next execution. How to fix it: switch to Define below and write an expression in Key that points to a stable identity of the real customer, not to the chat session's sessionId.
Switching to a persistent memory node without also changing the Key (conceptual). What happens: someone migrates from Simple Memory to Postgres Chat Memory to "fix" memory getting lost on every restart, but leaves Session ID on Define below with the same expression as before — {{ $json.chatSessionId }}, the widget's ephemeral id. The history now does survive a restart, but it still resets every time the customer opens a new tab, because storage was never the whole problem. Why it happens: "persistent memory" sounds like a single improvement, and it's easy to consider scope solved just because you changed the node. They're the same two independent decisions from lesson 1 of the module — changing one doesn't fix the other. How to spot it: if the customer comes back the next day and the agent doesn't recognize yesterday's conversation, despite having Postgres or Redis connected, check what expression the Key has — it's probably still pointing to something that changes every session, not to the customer. How to fix it: the Key has to always resolve to the same value for the same real customer, no matter when or from what device they write — a phone number, an account ID, a verified email.
Assuming Redis cleans up old history on its own (practical). What happens: someone configures Redis Chat Memory expecting inactive conversations to disappear automatically after a reasonable time, and months later the Redis database keeps growing with no limit, with thousands of customer sessions that never wrote back. Why it happens: Session Time To Live exists exactly for this, but its default value is 0 — "never expires" — the same permanent behavior as Postgres. Nothing cleans itself up unless you configure it. How to spot it: check the Redis database's size and how many session keys have no recent activity; if sessionTTL was never touched, it's still at 0. How to fix it: explicitly set sessionTTL in seconds based on how much inactivity time makes sense for your case — and if you need permanent, auditable retention, that's precisely the sign that Postgres, not Redis, is the right node.
Exercises
Exercise 1 — Predict the error. A workflow starts with a Webhook node (not a Chat Trigger) that receives messages from a contact form. Someone connects Postgres Chat Memory to the agent and leaves Session ID on Connected Chat Trigger Node, without touching anything else. What happens on the first execution, and why exactly that error and not another?
See solution
The execution fails with the No session ID found error. The node, in Connected Chat Trigger Node mode, looks for a sessionId field in the input or tries to read it from a connected Chat Trigger — but there's no Chat Trigger anywhere in this workflow, and the form's payload doesn't carry a field called sessionId either. Since it can't find either source, it has no identity at all to save or look up the history with, so it stops with that error instead of guessing a value.
Why it works: the Session ID selector isn't decorative — it determines where the value indexing all of memory comes from, and if that source doesn't exist in the workflow, there's no reasonable default n8n can use instead.
Exercise 2 — Choose the node and configuration. A billing support agent for a subscription app receives messages via Webhook, with {{ $json.body.accountId }} available in every payload. The compliance team asks that any conversation be auditable with SQL up to a year later. Write the complete Session ID and Key configuration, and decide between Postgres Chat Memory and Redis Chat Memory, justifying your answer with what you learned in this lesson.
See solution
Session ID = "Define below", Key = "{{ $json.body.accountId }}" — it's the real customer's stable identity, not dependent on the chat session. For the store, Postgres Chat Memory: the explicit requirement to audit with SQL up to a year later rules out Redis, which isn't built to be queried with SQL and whose sessionTTL would serve exactly the opposite of what compliance is asking for — deleting the history, not keeping it. Postgres Chat Memory doesn't delete anything on its own, so a year of history stays available unless someone explicitly removes it.
Why it works: the question that decides between the two nodes isn't which one is "better" in the abstract, but what the use case needs — permanent, queryable retention (Postgres) versus automatic expiration and speed (Redis).
Exercise 3 — Diagnose a memory leak between customers. A support agent for a store chain uses Postgres Chat Memory with Session ID = "Define below" and Key = "{{ $json.body.storeId }}" — the ID of the store the customer wrote to, not something about the customer themselves. Two different shoppers at the same store report the bot mixing up their orders. What's the cause, and how would you fix it?
See solution
The cause is that storeId doesn't identify a customer — it identifies the store. Every shopper writing to the same store shares the same session_id in the table, so Postgres Chat Memory treats them all as if they were one single continuous conversation: one shopper's history mixes with the next one's who writes to that store. The fix is to change Key to something that identifies the individual customer — their phone number or their account ID — not the channel or the store they came in through.
Why it works: a stable Key solves the persistence-over-time problem, but only if it's also unique per customer. Stable but shared across several people is still the same underlying mistake as using a fixed value — everyone falls into the same bucket.
Summary and next step
You now know how to resolve persistent memory's two complete decisions: the Session ID selector decides where the identity that groups the history comes from — Connected Chat Trigger Node for a Chat Trigger's ephemeral sessionId, Define below with a stable Key when you need to recognize the same real customer across sessions — and the node you choose — Postgres Chat Memory or Redis Chat Memory — decides where that history lives and for how long, with sessionTTL as the only real behavioral difference between the two.
Before moving on you should be able to: explain what error n8n throws if you leave Session ID at its default value with no Chat Trigger connected and why; write a Key expression that uniquely and stably identifies a real customer, not a session or a shared channel; and decide between Postgres and Redis given a retention or audit requirement.
What you still haven't resolved is what happens inside an active conversation with several turns in a row: how the agent chains one question to the previous one, resolves references like "the previous order" without the customer repeating the number, and holds the thread when the conversation changes topic and comes back. With memory already persisting and separated by customer, that's exactly what you'll see in the next lesson.
Resources
- Postgres Chat Memory node — n8n Docs — the node's complete reference:
Session ID,Table Name, andContext Window Length, and the warning that sharing memory between nodes requires an explicitKey. - Redis Chat Memory node — n8n Docs — the node's reference, including
Session Time To Liveand its default behavior. - Chat Trigger node — n8n Docs — where the
sessionIdtheConnected Chat Trigger Nodeoption looks for comes from, and the relatedLoad Previous Sessionoption. - Postgres credentials — n8n Docs — the connection fields (host, database, user, password) Postgres Chat Memory needs.
- Redis credentials — n8n Docs — the connection fields Redis Chat Memory needs, including the database number.
- Enable queue mode — n8n Docs — why a deployment with several workers is exactly the scenario where lesson 3's RAM memory fails and this lesson's doesn't.