DEV Community

Alex Kane
Alex Kane

Posted on

Build an n8n AI agent that remembers everything — persistent memory across runs (free workflow JSON)

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

  1. Receives a user message (via webhook — works with Telegram, Slack, your app, anything)
  2. Fetches conversation history for that user/session from a Postgres (or Supabase) table
  3. Formats the context as a messages array Claude understands
  4. Calls Claude with full conversation history
  5. Saves the new turn back to the database
  6. 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" }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

No special vector DB, no embeddings. Simple SQL — works with any Postgres-compatible DB including Supabase (free tier is plenty).


Setup steps

  1. Import the JSON into n8n (Workflows → Import from clipboard)
  2. Create Postgres credentials in n8n (or use Supabase — same schema)
  3. Run the SQL above to create the table
  4. Set ANTHROPIC_API_KEY in n8n Variables (Settings → Variables)
  5. Activate the workflow
  6. 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!"
Enter fullscreen mode Exit fullscreen mode

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)