How to Build an AI Agent in n8n 2.45: Tools, Memory and MCP (2026)
This guide is for anyone who wants a working n8n AI agent that does more than chat: it calls an HTTP API, runs a small piece of code, looks things up in your own database through a sub-workflow, uses a remote MCP server, and remembers each conversation in Postgres. At the end you will have that agent running locally on n8n 2.45.7 (the stable release of 30 October 2026), a curl command that talks to it, and an error path that tells you when it fails. I checked node names, parameters and error strings against the n8n source at the n8n@2.45.7 tag. You need Docker with Compose v2, and an API key for OpenAI, Anthropic or another provider n8n supports. n8n 3.0 is in beta (3.0.1 shipped as a pre-release on 28 October); everything below is on the 2.x line, and I note where 3.0 will differ.
Step 1: Run n8n 2.45.7 and Postgres with Docker Compose
One Postgres instance holds two databases: n8n for n8n itself, and agent_memory for the agent's chat history and a small runbooks table. Keeping the agent's data in a separate database with its own role means the agent's credential cannot read n8n's tables. This is a local setup; for production, follow the queue mode guide instead.
mkdir n8n-agent && cd n8n-agent
cat > .env <<EOF
POSTGRES_PASSWORD=$(openssl rand -hex 16)
AGENT_DB_PASSWORD=$(openssl rand -hex 16)
N8N_ENCRYPTION_KEY=$(openssl rand -hex 32)
EOF
chmod 600 .env
# init-agent-db.sh: runs once, when the Postgres volume is first created
cat > init-agent-db.sh <<'EOF'
#!/bin/sh
set -e
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE ROLE agent LOGIN PASSWORD '${AGENT_DB_PASSWORD}';
CREATE DATABASE agent_memory OWNER agent;
EOSQL
EOF
chmod +x init-agent-db.sh
# compose.yaml (n8n 2.45.7, Postgres 17.11)
services:
postgres:
image: postgres:17.11-alpine
restart: unless-stopped
environment:
POSTGRES_USER: n8n
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: n8n
AGENT_DB_PASSWORD: ${AGENT_DB_PASSWORD} # read by init-agent-db.sh
volumes:
- pgdata:/var/lib/postgresql/data
- ./init-agent-db.sh:/docker-entrypoint-initdb.d/10-agent-db.sh:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U n8n -d n8n"]
interval: 5s
timeout: 5s
retries: 10
n8n:
image: n8nio/n8n:2.45.7 # pin; never :latest
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
ports:
- "127.0.0.1:5678:5678" # localhost only
environment:
DB_TYPE: postgresdb
DB_POSTGRESDB_HOST: postgres
DB_POSTGRESDB_DATABASE: n8n
DB_POSTGRESDB_USER: n8n
DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}
N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY} # back this up; credentials are encrypted with it
GENERIC_TIMEZONE: Europe/London
N8N_DIAGNOSTICS_ENABLED: "false"
volumes:
- n8ndata:/home/node/.n8n
volumes:
pgdata: {}
n8ndata: {}
docker compose up -d
docker compose exec n8n n8n --version
# 2.45.7
docker compose exec postgres psql -U n8n -d n8n -c '\l agent_memory'
Open http://localhost:5678, create the owner account, and you are in the editor. Now seed the runbooks table the agent will query in Step 6:
docker compose exec -T postgres psql -U agent -d agent_memory <<'EOF'
CREATE TABLE runbooks (
service text PRIMARY KEY,
owner text NOT NULL,
runbook_url text NOT NULL,
notes text
);
INSERT INTO runbooks VALUES
('payments-api', 'team-payments', 'https://wiki.example.com/rb/payments-api', 'Restart is safe; drain first'),
('search', 'team-discovery', 'https://wiki.example.com/rb/search', 'Reindex takes about an hour');
EOF
Step 2: Add model and database credentials
In the left sidebar choose Credentials, then Create credential. Add three:
- OpenAI (or Anthropic, or any provider in the chat model list). Paste the API key. n8n 2.45.7 ships chat model nodes for OpenAI, Anthropic, Azure OpenAI, AWS Bedrock, Google Gemini and Vertex, Mistral, Groq, DeepSeek, OpenRouter, Ollama and more.
- A second model provider, optional, for the fallback in Step 3.
- Postgres, pointing at the agent database:
Host: postgres # the Compose service name, not localhost
Database: agent_memory
User: agent
Password: <AGENT_DB_PASSWORD from .env>
Port: 5432
SSL: disable # local only; use require in production
Credentials are encrypted with N8N_ENCRYPTION_KEY. Lose that key and every saved credential is unreadable, so copy .env somewhere safe now.
Step 3: Create the Chat Trigger and AI Agent nodes
Create a workflow and add the Chat Trigger node (@n8n/n8n-nodes-langchain.chatTrigger). Set:
Make Chat Publicly Available: On # needed for the curl test in Step 8
Mode: Embedded Chat
Authentication: Basic Auth # never None on a reachable instance
Options > Response Mode: When Last Node Finishes
Options > Allowed Origins (CORS): http://localhost:5678
The Chat Trigger docs note that every message runs the workflow and counts as one execution, which matters if you later move to n8n Cloud.
Add an AI Agent node after it. At 2.45.7 its default type version is 3.1, and every AI Agent node behaves as a Tools Agent: the old agent-type selector was deprecated in 1.82.0 and the docs say the v1 node goes away in 3.0. Set:
Source for Prompt (User Message): Connected Chat Trigger Node
Require Specific Output Format: Off (Step 7 turns this on)
Enable Fallback Model: On (if you added a second provider)
Options > System Message: (below)
Options > Max Iterations: 6 # default is 10
Options > Enable Streaming: Off # matches "When Last Node Finishes"
You are an on-call helper for a platform team.
- For "latest release" questions, call get_latest_release. Never guess versions.
- For "how far behind is X" questions, call compare_versions after you have both versions.
- For questions about one of our services, call lookup_runbook with the service name.
- For questions about how an open-source repository works, use the DeepWiki tools.
If a tool returns an error or nothing, say so plainly. Answer in at most five sentences.
Now click the Chat Model connector under the agent and pick your provider's chat model node, select the credential and a current model. If you enabled the fallback, a second Fallback Model connector appears; attach the other provider. The agent retries on the fallback when the primary call fails, which covers provider outages and rate limits without a second agent.
Step 4: Add Postgres Chat Memory
Click the agent's Memory connector and choose Postgres Chat Memory (memoryPostgresChat, type version 1.4 at this release). Select the Postgres credential from Step 2 and set:
Session ID: Connected Chat Trigger Node # reads sessionId from the trigger
Table Name: n8n_chat_histories # default; created if missing
Context Window Length: 10 # past interactions sent to the model; default 5
The node creates the table on first use through LangChain's Postgres message history, with three columns: id SERIAL, session_id VARCHAR(255) and message JSONB. Two things the defaults do not give you are an index on session_id and any timestamp to expire old conversations. Add both once the table exists (after your first chat message):
-- run in agent_memory as the agent role
CREATE INDEX IF NOT EXISTS n8n_chat_histories_session_idx
ON n8n_chat_histories (session_id);
ALTER TABLE n8n_chat_histories
ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT now();
-- inspect the last few stored messages
SELECT id, session_id, message->>'type' AS role,
left(message->'data'->>'content', 60) AS content
FROM n8n_chat_histories ORDER BY id DESC LIMIT 6;
-- retention: delete conversations idle for 30 days (schedule this)
DELETE FROM n8n_chat_histories
WHERE session_id IN (
SELECT session_id FROM n8n_chat_histories
GROUP BY session_id HAVING max(created_at) < now() - interval '30 days');
The insert only names session_id and message, so the default fills created_at. One version detail: from memory node 1.4, a session taken from the Chat Trigger is scoped to that one memory node. To share a conversation between two agents in the same workflow, set Session ID to Define below and use the same key expression in both, for example {{ $('Chat Trigger').item.json.sessionId }}. For why chat history alone is not long-term memory, see the agent memory post.
Step 5: Add an HTTP Request tool and a Code tool
Click the agent's Tool connector and add HTTP Request Tool. The older dedicated HTTP tool node is hidden in 2.45.7; the regular HTTP Request node now works in tool mode. Configure:
Name/Description: get_latest_release: Returns the latest published release of a GitHub
repository. Input is the repository as owner/name, e.g. kubernetes/kubernetes.
Method: GET
URL: https://api.github.com/repos/{{ $fromAI('repo', 'GitHub repository as owner/name', 'string') }}/releases/latest
Send Headers: On Accept = application/vnd.github+json
Optimize Response: On Expected Response Type: JSON
Include Fields: Selected Fields: tag_name,published_at,html_url
$fromAI(key, description, type, defaultValue) marks the part of the field the model fills in; keys are 1 to 64 letters, digits, underscores or hyphens, and types are string, number, boolean or json. Optimize Response matters more than it looks: the raw releases payload includes the full release notes, and every byte of a tool result goes back into the model's context. Here is what the tool sees after trimming, from a real call I made today:
curl -s https://api.github.com/repos/kubernetes/kubernetes/releases/latest \
| jq '{tag_name, published_at, html_url}'
# {
# "tag_name": "v1.37.2",
# "published_at": "2026-10-28T04:37:53Z",
# "html_url": "https://github.com/kubernetes/kubernetes/releases/tag/v1.37.2"
# }
Unauthenticated GitHub calls are rate-limited per IP; add a GitHub credential under Authentication if the agent will be busy.
Add a second tool, Code Tool. Name it compare_versions, describe it as "Compares two semantic versions. Input: two versions separated by a comma, e.g. 1.33.4, 1.36.1", leave Language as JavaScript, and paste:
// The tool's input arrives as `query` (a string). Return a single string.
const [a, b] = String(query).split(',').map(s => s.trim().replace(/^v/, ''));
if (!a || !b) return 'Pass two versions separated by a comma, e.g. "1.34.2, 1.36.0"';
const pa = a.split('.').map(Number), pb = b.split('.').map(Number);
if ([...pa, ...pb].some(Number.isNaN)) return `Not a numeric version: ${a} or ${b}`;
const [ma, mi] = [pb[0] - pa[0], pb[1] - pa[1]];
if (ma !== 0) return `${b} is ${Math.abs(ma)} major version(s) ${ma > 0 ? 'newer' : 'older'} than ${a}`;
if (mi !== 0) return `${b} is ${Math.abs(mi)} minor version(s) ${mi > 0 ? 'newer' : 'older'} than ${a}`;
return `${a} and ${b} share major.minor; patch difference is ${(pb[2] ?? 0) - (pa[2] ?? 0)}`;
I ran the same body under Node 20 with the input v1.33.4, 1.36.1 and it returned 1.36.1 is 3 minor version(s) newer than 1.33.4. Note that $fromAI() does not work in the Code tool; it receives whatever the model sends as query. If you need typed fields, turn on Specify Input Schema. Code runs in n8n's task runner, and since 2.0 N8N_BLOCK_ENV_ACCESS_IN_NODE defaults to true, so $env is blocked inside it. That is the right default: put secrets in credentials, not environment variables.
Step 6: Add a sub-workflow tool and an MCP Client tool
Tools that touch your own data are best built as sub-workflows: they can be tested on their own, and the agent sees only the inputs you define. Create a new workflow called Lookup runbook:
- Add When Executed by Another Workflow (Execute Workflow Trigger). Set Input data mode to Define using fields below and add one field:
service, type String. - Add a Postgres node with the agent credential, operation Execute Query:
SELECT service, owner, runbook_url, notes
FROM runbooks
WHERE service = $1;
-- Options > Query Parameters: {{ $json.service }}
- Save it and Publish it. Since 2.0, publishing replaces the old active toggle, and an unpublished sub-workflow fails in production.
Back in the agent workflow, add Call n8n Workflow Tool. Name it lookup_runbook, describe it as "Looks up the owner, runbook URL and notes for one of our services by exact service name", set Source to Database, pick Lookup runbook, click Refresh next to Workflow Inputs, and use the "Let the model define this parameter" button on service, which writes a $fromAI() expression for you. The parent receives the output of the sub-workflow's last node, so the agent gets the query row.
Last, add MCP Client Tool (mcpClientTool, type version 1.4). I use DeepWiki's public server because it needs no key, but the shape is the same for any remote server, including your own; the MCP server guide shows how to write one.
Endpoint: https://mcp.deepwiki.com/mcp
Server Transport: HTTP Streamable # SSE is marked deprecated in this node
Authentication: None # Bearer, Header, Multiple Headers or MCP OAuth2 for private servers
Tools to Include: Selected -> ask_wiki_question, read_wiki_structure
Options > Timeout: 60000 # ms, the default
You can check what a server exposes before wiring it in. This is the call n8n makes to list tools, and the real result today:
curl -s -X POST https://mcp.deepwiki.com/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' \
| sed -n 's/^data: //p' | jq -r '.result.tools[].name'
# ask_wiki_question
# read_wiki_contents
# read_wiki_structure
Selecting tools rather than including all of them keeps the tool list short, which makes the model's choice easier and the prompt cheaper.
Step 7: Return structured output with the Structured Output Parser
A chat reply is fine for people. When the agent feeds other nodes (an If node, a Postgres insert, a Slack message with fields) you want JSON. On the AI Agent set Require Specific Output Format to On, then attach Structured Output Parser to the new Output Parser connector:
{
"service": "payments-api",
"owner": "team-payments",
"runbook_url": "https://wiki.example.com/rb/payments-api",
"action": "restart",
"confidence": "high"
}
With Schema Type set to Generate From JSON Example, n8n uses the property names and types and ignores the values, and from parser version 1.3 every field is required. For enums or optional fields switch to Define using JSON Schema; $ref is not supported. Downstream nodes read the result as {{ $json.output.owner }}. If the model returns something that does not match, the agent fails with "Model output doesn't fit required format". Turning on Auto-Fix Format retries with one more model call, which costs tokens but usually fixes a stray field.
Step 8: Add error handling and test the agent
Agents fail in more ways than workflows: the provider times out, a tool returns a 404, the model loops. Set these in each node's Settings tab:
Chat model node: Retry On Fail: On Max Tries: 3 Wait Between Tries (ms): 2000
HTTP Request Tool: Retry On Fail: On Max Tries: 2
AI Agent: On Error: Continue (using error output) # route failures to a polite reply
Then create a workflow called Agent errors that starts with an Error Trigger, add a Slack, email or HTTP node, and in the agent workflow's Settings choose it as the Error workflow. Per the error handling docs, it receives execution.id, execution.url, execution.error.message, execution.lastNodeExecuted and the workflow name. A useful alert body:
Agent failed: {{ $json.workflow.name }}
Node: {{ $json.execution.lastNodeExecuted }}
Error: {{ $json.execution.error.message }}
Execution: {{ $json.execution.url }}
Publish the agent workflow, copy the Chat URL from the Chat Trigger, and test it. The body fields are the ones the Chat Trigger reads; sessionId is what memory keys on.
curl -s -u 'chatuser:chatpass' -X POST \
"http://localhost:5678/webhook/<webhook-id>/chat" \
-H 'Content-Type: application/json' \
-d '{"action":"sendMessage","sessionId":"oncall-1","chatInput":"What is the latest Kubernetes release, and how far behind is 1.34.3?"}'
# Response shape with "When Last Node Finishes": {"output":"<the agent's answer>"}
Send a follow-up with the same sessionId ("and who owns payments-api?") and check that the memory query from Step 4 shows four rows for oncall-1. Open Executions to see each tool call and its input; that log is the first place to look when an answer is wrong.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| "A Chat Model sub-node must be connected" | No model attached to the agent | Attach a chat model to the Chat Model connector |
| "No prompt specified" | Prompt source is the Chat Trigger but the input came from another node | Set Source for Prompt to Define below and map the text field |
| "400 Invalid value for 'content': expected a string, got null" | Prompt expression resolves to null | Fix the expression or strip null chatInput values upstream |
| "No session ID found" in Postgres Chat Memory | Input has no sessionId field (common with Webhook or Schedule triggers) |
Set Session ID to Define below with an explicit key expression |
| "Max iterations (6) reached" | Model keeps calling tools, often from a vague tool description or a tool that errors | Sharpen descriptions, fix the failing tool, then raise Max Iterations only if needed |
| "Model output doesn't fit required format" | Output does not match the parser schema | Simplify the schema, enable Auto-Fix Format, or make fields optional in JSON Schema |
| Tool returns "Workflow is not active and cannot be executed" | Sub-workflow not published | Publish the sub-workflow and check it has executions |
| MCP Client lists no tools or returns 404/405 | Wrong transport or endpoint path | Use HTTP Streamable with the server's /mcp URL; SSE only for old servers |
| Agent forgets the conversation between messages | New sessionId per request, or two memory nodes on scoped sessions |
Reuse the same sessionId; share sessions with a Define below key |
Code tool fails reading $env |
N8N_BLOCK_ENV_ACCESS_IN_NODE is true by default since 2.0 |
Use a credential or pass the value through the tool input |
FAQ
Do I need a vector database for n8n agent memory?
No. Postgres Chat Memory stores the conversation transcript and sends the last N interactions to the model, which is what most chat agents need. A vector store is for retrieving documents or facts by meaning; add one as a tool (n8n has a Vector Store tool) when the agent has to search knowledge, not recall the chat.
What is the difference between Simple Memory and Postgres Chat Memory in n8n?
Simple Memory (the buffer window node) keeps history inside the n8n instance and is fine for testing. Postgres Chat Memory writes every message to a table you control, survives restarts, works across queue-mode workers, and can be inspected, backed up and expired with SQL.
Can n8n expose my agent as an MCP server?
Yes. The MCP Server Trigger node turns a workflow's tools into an MCP server that clients such as Claude or Cursor can call. That is the reverse of this guide's MCP Client Tool, which lets the n8n agent call someone else's server.
Will this workflow work on n8n 3.0?
The nodes in this guide are current versions on 2.45.7, so they should carry over, but 3.0 is still a pre-release and its docs say the v1 AI Agent node is removed. Workflows copied from old templates that use the agent-type selector need rebuilding with a new AI Agent node before you upgrade.
When I review an n8n agent someone else built, I check three things first: the Max Iterations value (the default of 10 is generous for most tasks), whether each credential is scoped to only what its tool needs (the runbooks tool here can read one database, not n8n's), and whether an error workflow exists at all. Then I read ten executions end to end, because the tool inputs tell you more about prompt quality than the answers do. For a comparison of n8n with Zapier, Copilot Studio and the rest, see the no-code agent builders comparison; if the agent outgrows the canvas, the Python tool-calling guide is the code-first version. Official references: the AI Agent node, its common issues, Postgres Chat Memory, Call n8n Workflow Tool, MCP Client Tool, Structured Output Parser and the 2.0 breaking changes.
Related: How to Self-Host n8n with Docker Compose in Queue Mode (2026), How to Build an AI Agent With Tool Calling in Python (2026) and Agent Memory in 2026: Vendors Claim 92.5 on LoCoMo, a Plain Filesystem Scored 74, and Every Memory Product Lost to BM25.
I'm Ajay Kumar — I build and operate PandaStack, an open-source Firecracker microVM cloud for AI agents, and write about the infrastructure and tooling around it.
Need this kind of infrastructure work? See what I do or email hello@ajayk.sh.
Related
No-Code AI Agent Builders Compared (2026): 7 Tools, One Shutting Down
No-code AI agent builders compared for 2026: n8n, Zapier Agents, Make, Gumloop, Relevance AI, Copilot Studio and OpenAI Agent Builder (shutting down 30 November). Billing units, real prices, self-hosting, a per-message cost worked through, and when to write code instead.
11 minOct 31, 2026How to Secure a Supabase App Before Launch: RLS Checklist (2026)
A step-by-step Supabase security checklist for 2026: enable RLS on every table, write auth.uid() policies, handle the 30 October grants change, move to publishable and secret keys, lock storage buckets, protect Edge Function secrets and test as anon.
13 minOct 5, 2026Workflow Automation in 2026: n8n Published 196 Advisories in Nine Months, 59,558 Hosts Sat Exposed, and the Code Alternative Raised at $12.55B
The no-code automation tools became agent runtimes this year and the security record followed: n8n went from a EUR 250M valuation to a reported $5.2B while publishing 196 GitHub security advisories in 2026 (19 critical), a CVSS 10.0 unauthenticated flaw left 59,558 hosts exposed per Shadowserver, and CISA added one n8n and six Langflow CVEs to its exploited list. Zapier MCP counts 25 million tool calls, Copilot Studio bills 5 credits per agent action, Agentforce ARR passed $1.5B, and Temporal raised $550M at $12.55B with 4,300 paying customers. Plus a hardened n8n queue-mode compose file.
15 min