If you've ever built an AI chatbot or automation that uses Claude or GPT â you've hit the wall.
Every run starts fresh. The AI has no idea what happened last time. Your customer asks a follow-up question and gets a blank stare.
The fix? Give your n8n AI agent persistent memory.
This workflow stores every conversation turn in a database and retrieves relevant context on the next run â so your agent actually remembers who it's talking to.
What it does
- Receives a user message (via webhook â works with Telegram, Slack, your app, anything)
- Fetches conversation history for that user/session from a Postgres (or Supabase) table
- Formats the context as a messages array Claude understands
- Calls Claude with full conversation history
- Saves the new turn back to the database
- Returns the reply to the caller
The result: a stateful AI agent that remembers names, preferences, previous requests, and ongoing tasks â across separate runs, days apart.
The workflow (7 nodes)
Webhook â Postgres (fetch history) â Code (format context) â HTTP Request (Claude API) â Postgres (save turn) â Respond to Webhook
Full workflow JSON â copy and import into n8n:
{
"name": "Claude AI Agent Memory",
"nodes": [
{
"parameters": {
"path": "ai-agent",
"responseMode": "responseNode",
"options": {}
},
"id": "node-webhook",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [240, 300],
"webhookId": "ai-agent-memory"
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT role, content, created_at FROM agent_memory WHERE session_id = '{{ $json.session_id }}' ORDER BY created_at DESC LIMIT 20",
"options": {}
},
"id": "node-fetch-history",
"name": "Fetch History",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [460, 300],
"credentials": { "postgres": { "id": "1", "name": "Postgres" } }
},
{
"parameters": {
"jsCode": "const history = $input.all().map(item => item.json);\nconst userMessage = $('Webhook').first().json.message;\nconst sessionId = $('Webhook').first().json.session_id || 'default';\n\n// Reverse so oldest first (Claude expects chronological order)\nconst sorted = history.reverse();\n\nconst messages = [\n ...sorted.map(row => ({ role: row.role, content: row.content })),\n { role: 'user', content: userMessage }\n];\n\nreturn [{ json: { messages, session_id: sessionId, user_message: userMessage } }];"
},
"id": "node-format-context",
"name": "Format Context",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [680, 300]
},
{
"parameters": {
"method": "POST",
"url": "https://api.anthropic.com/v1/messages",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{ "name": "x-api-key", "value": "={{ $vars.ANTHROPIC_API_KEY }}" },
{ "name": "anthropic-version", "value": "2023-06-01" },
{ "name": "content-type", "value": "application/json" }
]
},
"sendBody": true,
"bodyParameters": {
"parameters": [
{ "name": "model", "value": "claude-3-5-haiku-20241022" },
{ "name": "max_tokens", "value": "=1024" },
{ "name": "system", "value": "You are a helpful assistant. You have a memory of previous conversations with this user. Be concise and helpful." },
{ "name": "messages", "value": "={{ $json.messages }}" }
]
},
"options": {}
},
"id": "node-claude",
"name": "Call Claude",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [900, 300]
},
{
"parameters": {
"jsCode": "const claudeResponse = $input.first().json;\nconst context = $('Format Context').first().json;\nconst assistantReply = claudeResponse.content[0].text;\n\nreturn [{\n json: {\n session_id: context.session_id,\n user_message: context.user_message,\n assistant_reply: assistantReply\n }\n}];"
},
"id": "node-extract-reply",
"name": "Extract Reply",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [1120, 300]
},
{
"parameters": {
"operation": "executeQuery",
"query": "INSERT INTO agent_memory (session_id, role, content, created_at) VALUES ('{{ $json.session_id }}', 'user', '{{ $json.user_message }}', NOW()); INSERT INTO agent_memory (session_id, role, content, created_at) VALUES ('{{ $json.session_id }}', 'assistant', '{{ $json.assistant_reply }}', NOW());",
"options": {}
},
"id": "node-save-turn",
"name": "Save Turn",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [1340, 300],
"credentials": { "postgres": { "id": "1", "name": "Postgres" } }
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ { reply: $('Extract Reply').first().json.assistant_reply } }}"
},
"id": "node-respond",
"name": "Respond",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [1560, 300]
}
],
"connections": {
"Webhook": { "main": [[{ "node": "Fetch History", "type": "main", "index": 0 }]] },
"Fetch History": { "main": [[{ "node": "Format Context", "type": "main", "index": 0 }]] },
"Format Context": { "main": [[{ "node": "Call Claude", "type": "main", "index": 0 }]] },
"Call Claude": { "main": [[{ "node": "Extract Reply", "type": "main", "index": 0 }]] },
"Extract Reply": { "main": [[{ "node": "Save Turn", "type": "main", "index": 0 }]] },
"Save Turn": { "main": [[{ "node": "Respond", "type": "main", "index": 0 }]] }
},
"settings": { "executionOrder": "v1" }
}
Database setup (2 minutes)
Run this in your Postgres or Supabase SQL editor:
CREATE TABLE agent_memory (
id SERIAL PRIMARY KEY,
session_id TEXT NOT NULL,
role TEXT NOT NULL CHECK (role IN ('user', 'assistant')),
content TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_agent_memory_session ON agent_memory(session_id, created_at DESC);
No special vector DB, no embeddings. Simple SQL â works with any Postgres-compatible DB including Supabase (free tier is plenty).
Setup steps
- Import the JSON into n8n (Workflows â Import from clipboard)
- Create Postgres credentials in n8n (or use Supabase â same schema)
- Run the SQL above to create the table
- Set
ANTHROPIC_API_KEYin n8n Variables (Settings â Variables) - Activate the workflow
- Test with a POST to your webhook URL:
# First message
curl -X POST https://your-n8n.com/webhook/ai-agent \
-H "Content-Type: application/json" \
-d '{"session_id": "user-123", "message": "My name is Sarah and I run a bakery"}'
# Second call â agent remembers
curl -X POST https://your-n8n.com/webhook/ai-agent \
-H "Content-Type: application/json" \
-d '{"session_id": "user-123", "message": "What kind of business do I have?"}'
# Returns: "You mentioned you run a bakery, Sarah!"
Pro tips
Limit context window cost â The workflow fetches the last 20 turns. For long sessions, change LIMIT 20 to LIMIT 10 or add a token counter Code node before the Claude call.
Add a system prompt per session â Store a system_prompt column per session_id so each user gets a tailored persona (customer support bot, personal assistant, sales agent).
Use with Telegram â Replace the Webhook trigger with n8n's Telegram Trigger node. Use $json.message.chat.id as the session_id. Instant persistent Telegram AI bot.
Summarize old history â For very long sessions, add a Code node that summarizes turns older than 7 days into a single summary entry. Cuts costs significantly.
Swap Claude for OpenAI â Replace the HTTP Request node with the native OpenAI node. The messages array format is identical â no other changes needed.
Multiple agent personas â Pass a bot_id param alongside session_id. Filter by both in your SQL query. One workflow, many agents.
Get the full workflow pack
This workflow is part of the FlowKit n8n Template Pack â 15 production-ready n8n workflows for automating real business tasks.
Get all 15 at: stripeai.gumroad.com
Includes this AI Agent Memory workflow plus Email Auto-Responder, Lead Capture to CRM, Customer Feedback Analyzer, Price Monitor, and 10 more â all with full JSON and step-by-step setup docs.
Questions or stuck on setup? Drop a comment below.
Top comments (0)