Building Production AI Agents in n8n

Last month I rebuilt a client's agent stack that had been limping along on a single Python service with a Celery queue and a Redis-backed memory layer. It was doing the job, but every change required a deploy, and their ops team could not see what was happening inside a run. We moved the orchestration into n8n, kept the heavy lifting (embeddings, tool calls, RAG) in dedicated services, and shipped something their non-engineers could actually read. Uptime went from "mostly fine" to boring. That is the goal.
This post is the blueprint I use when I wire n8n into a production agent stack. Not the demo version. The one that survives a Monday morning traffic spike, a rate-limited model provider, and a webhook that decides to fire twice.
When n8n is the right tool (and when it is not)
n8n is the right tool when you need visible orchestration: a workflow that a solutions engineer or ops person can open, follow, and debug without reading Python. It shines for glue work between SaaS systems, LLM calls, and databases. It is the wrong tool when you need sub-100ms latency, tight streaming to a frontend, or a genuinely stateful multi-agent loop with dozens of turns per run.
Here is how I decide:
| Use case | n8n | Custom code (Node/Python) |
|---|---|---|
| CRM + LLM + email/Slack glue | Yes | Overkill |
| Scheduled research + publish pipelines | Yes | Fine, but slower to iterate |
| Real-time chat with streaming tokens | No | Yes |
| Complex agent loops with 20+ tool calls | Hybrid | Yes |
| Compliance-heavy workflows needing audit trails | Yes | Yes, more work |
The pattern I recommend most often: n8n as the outer orchestrator, a small FastAPI or Node service as the inner brain. n8n handles triggers, retries, human approvals, and hand-offs. The service handles anything that needs real code (vector search with RRF, LangGraph loops, prompt caching logic). More on that below.
The reference architecture I use
Every production n8n agent I have shipped follows the same skeleton. Once you have it, you stop rewriting the boring parts.
Webhook (auth + idempotency)
-> Validate & normalize
-> Route (intent or type)
-> Agent branch A (LLM + tools + memory)
-> Agent branch B (different model or role)
-> Post-process & guardrails
-> Hand-off (queue, DB write, downstream API)
-> Notify (Slack/email on failure, log on success)
Error Trigger workflow -> DLQ + alert
Six blocks. That is it. Everything else is a variation.
1. Webhook trigger with real authentication
Do not ship an n8n webhook with just "Header Auth" and hope. I always add three things at the entry point:
- HMAC signature verification on the raw body. A Function node computes the SHA-256 HMAC using a secret from credentials and compares it to the
X-Signatureheader. If it does not match, respond 401 and stop. - An idempotency key stored in a Postgres or Redis table with a short TTL. Webhooks retry. Stripe retries. Zendesk retries. GitHub retries. If you call an LLM twice for the same event, you paid twice.
- A trace ID generated on entry (I use
nanoidin a Function node) that follows the item through every downstream node and into logs. When something breaks at 3am, you find the run by trace ID in one query.
// Function node: entry guard
const crypto = require('crypto');
const signature = $input.first().headers['x-signature'];
const raw = JSON.stringify($input.first().body);
const expected = crypto
.createHmac('sha256', $credentials.webhookSecret)
.update(raw)
.digest('hex');
if (signature !== expected) {
throw new Error('Invalid signature');
}
return [{
json: {
...$input.first().body,
traceId: require('nanoid').nanoid(),
receivedAt: new Date().toISOString(),
},
}];
Memory: what actually works in n8n
The default n8n memory nodes (Window Buffer, Postgres Chat Memory, Redis Chat Memory) are fine for chatbots. They are not fine for agents that need to remember facts across sessions, weeks, or users.
For anything beyond a single-session chat, I split memory into three layers:
- Short-term (window buffer): last N turns of the current conversation. Lives in the built-in memory node.
- Session state: structured JSON (current task, resolved entities, open questions) written to a Postgres row keyed by session ID. I use an HTTP Request or Postgres node, not the chat memory node, because I want to control the schema.
- Long-term semantic memory: pgvector table, populated by a separate summarization workflow that runs nightly. I do hybrid retrieval (BM25 full-text search plus vector similarity, fused with Reciprocal Rank Fusion) via a small FastAPI service the n8n workflow calls. n8n is a bad place to implement RRF. A 40-line FastAPI endpoint is a great place.
The trade-off is honesty about latency. Every memory read adds 100 to 400ms. For an unattended workflow that runs in the background, this does not matter. For a live agent that a user is watching, budget carefully: I usually skip long-term memory on the first turn and lazy-load it only when the model requests it via a recall_context tool.
LLM routing without lock-in
I never let a workflow depend on a single model provider. Providers rate-limit, deprecate models, and change pricing. The routing layer is a Switch node plus a small policy table.
My policy table lives in a Postgres row that a "Get Model Policy" HTTP node reads on each run. It looks like this:
| Task type | Primary | Fallback | Max tokens | Temp |
|---|---|---|---|---|
| Classification | Claude Haiku | GPT-4o-mini | 500 | 0.0 |
| Extraction | Claude Sonnet | GPT-4o | 2000 | 0.0 |
| Writing (long) | Claude Sonnet | GPT-4o | 4000 | 0.4 |
| Reasoning/tools | Claude Sonnet | GPT-4o | 4000 | 0.2 |
| Bulk/cheap | Local Llama via Ollama | Haiku | 1000 | 0.1 |
Two rules I enforce in the routing node:
- Never call the primary model without a fallback branch wired up. If the primary returns a 429 or 5xx, an Error Trigger sub-workflow re-routes to the fallback with the same input. No manual intervention.
- Log token counts and cost per call to a
llm_callstable (prompt tokens, completion tokens, model, latency, trace ID, cost in USD). This is the single most useful thing you can do for future you. When someone asks "why did our LLM bill triple?", you have an answer in one SQL query.
For a BizFlowAI ContentStudio-style pipeline running dozens of jobs a day, this table paid for itself in the first week. We caught a bad prompt that had accidentally set max_tokens: 8000 and was eating $40 a day on drafts that should have cost $2.
Error handling that does not lie
The default n8n error behavior is: node fails, workflow stops, you get an email if you configured one. That is not good enough for production.
Here is what I set up on every workflow:
- Continue On Fail enabled on any node that hits an external API, with an IF node right after checking for the error and routing to a dedicated error branch. This lets me distinguish "this item failed, others should proceed" from "the whole run should die".
- An Error Trigger workflow that catches uncaught failures, writes the full payload plus stack to a
dead_lettertable, posts to a Slack channel with the trace ID, and (for critical workflows) pages via a webhook to my incident tool. - Retry with exponential backoff on transient errors only. I do not blindly retry a 400 (bad request) because retrying broken input is expensive theater. I retry 429, 502, 503, 504 with a jittered backoff (2s, 5s, 15s).
- A daily reconciliation workflow that scans the dead letter table, groups by error type, and posts a summary. Silent failures are the worst kind. Force them to be loud.
One specific gotcha: n8n's built-in Retry On Fail is not jittered. If ten items all fail at the same second and retry three times each on the same schedule, you will hammer the downstream API in perfect lockstep and get rate-limited again. I roll my own retry with a random jitter of plus or minus 30 percent in a Wait node when it matters.
Hand-offs to downstream systems
Agents are not useful if their output sits in an n8n execution log. The hand-off is where value actually lands. Three patterns I use:
1. Fire-and-forget queue. The agent writes its result to a Postgres table with status = 'pending_review' or drops a message on SQS/Redis. A downstream consumer (another n8n workflow, or a proper service) picks it up. Good for anything with human-in-the-loop or asynchronous processing.
2. Direct API call with confirmation. The agent calls the downstream API (create ticket, send email, update CRM) and waits for a 2xx. If it fails, the error branch fires. Good for tight, small hand-offs where you want to know immediately if it worked.
3. Approval gate. The agent writes a proposed action to a table, posts a message to Slack with Approve/Reject buttons that hit a second n8n webhook. On approval, the second workflow executes the action. This is how I ship anything customer-facing on day one. You loosen the gate once you trust the agent, not before.
For the AWS + Zendesk serverless integration I built years ago (the one that hit first-ever SLA compliance for that team), the entire orchestration could have been an n8n workflow today. The Lambda + EventBridge design was right for the constraints then. For a similar problem now, I would start in n8n with a self-hosted deployment behind a VPC, and only drop to Lambda for the two or three pieces that actually needed it. Faster to ship, easier to hand off to the ops team.
Deployment: self-hosted, not cloud, for anything serious
For prototypes and internal tools, n8n Cloud is fine. For production workflows that touch customer data, handle real volume, or need custom nodes, I always self-host. On AWS this looks like:
- ECS Fargate for the n8n container, behind an ALB.
- RDS Postgres for the n8n database (never the built-in SQLite in production).
- ElastiCache Redis for the queue mode (n8n in queue mode with worker containers is what lets you scale horizontally).
- S3 for binary data offload.
- CloudWatch + a real log aggregator (I usually pipe to Grafana Loki or Datadog) because the n8n execution UI is not a log system.
Queue mode is the setting that separates "n8n runs my automations" from "n8n runs my business". Without it, one long-running workflow blocks everything else. With it, you have a main process handling triggers and a fleet of workers pulling jobs. Set EXECUTIONS_MODE=queue and run at least two workers from day one.
Cost for a moderate workload (a few thousand executions a day, mixed LLM and API work) sits around $150 to $300 a month in AWS infrastructure, plus the LLM bill. Not free, but predictable and it scales.
What I'd do on day one
If you are starting a real n8n agent build tomorrow, this is my order of operations:
- Draw the six-block skeleton on paper. Trigger, validate, route, agents, guardrails, hand-off. If you cannot draw yours in five minutes, the design is not clear enough yet.
- Set up self-hosted n8n in queue mode with Postgres and Redis. Do not start on Cloud and migrate later. Migrating credentials and executions is annoying.
- Build the error trigger workflow and the llm_calls logging table before the first agent. Observability first, features second.
- Ship one workflow end-to-end with an approval gate before you build the second. Get the deploy loop and monitoring working on something small.
- Only then, add memory, routing, fallbacks, and additional agents. The temptation to build the "full multi-agent system" on day one is how projects die.
One more opinion: keep prompts in a database or a git-versioned folder loaded via HTTP, not in n8n nodes. Editing a 2000-token system prompt inside a tiny n8n textarea is misery, and there is no diff view when someone changes it. Externalize prompts, version them, and load them at run time.
Wrapping up
n8n is not magic and it is not a toy. It is a solid orchestrator that lets you ship visible, maintainable AI workflows faster than pure code, if you respect the parts it is bad at and offload them cleanly. The blueprint above is the version I have arrived at after several client builds; yours will diverge in the details, and that is fine.
If you are building something in this shape and want another set of eyes, or you are stuck deciding between n8n, LangGraph, or a custom service for your agent stack, get in touch. I write more of these on the blog as I ship them.
Frequently asked questions
When should I use n8n for AI agent orchestration versus writing custom code?
I use n8n when I need visible orchestration that non-engineers can open, follow, and debug, glue work between SaaS systems, LLM calls, and databases, or scheduled research and publishing pipelines. I avoid n8n when I need sub-100ms latency, streaming tokens to a frontend, or complex stateful multi-agent loops with dozens of turns per run. My preferred pattern in production is n8n as the outer orchestrator handling triggers, retries, human approvals, and hand-offs, with a small FastAPI or Node service as the inner brain for real code like vector search or LangGraph loops. That hybrid gives you visibility without sacrificing the parts that need real engineering.
How do I secure an n8n webhook for a production AI agent?
Header Auth alone is not enough. I always add three things at the entry point: HMAC signature verification on the raw body using SHA-256 and a secret from credentials (respond 401 on mismatch), an idempotency key stored in Postgres or Redis with a short TTL to prevent double-processing when providers like Stripe or GitHub retry webhooks, and a trace ID generated on entry (I use nanoid) that follows the item through every downstream node and into logs. That last one is what lets you find a broken run at 3am with a single query.
How should I structure memory for an n8n AI agent that needs to remember things across sessions?
The built-in memory nodes are fine for single-session chatbots but not for agents that need to recall facts across sessions or users. I split memory into three layers: short-term (a window buffer of the last N turns in the built-in node), session state (structured JSON like current task and resolved entities written to a Postgres row keyed by session ID, with a schema I control), and long-term semantic memory (a pgvector table populated by a nightly summarization workflow). For retrieval I use hybrid search, BM25 plus vector similarity fused with Reciprocal Rank Fusion, but I run that in a small FastAPI service the workflow calls, because n8n is a bad place to implement RRF.
How do I avoid vendor lock-in when calling LLMs from n8n?
I never let a workflow depend on a single model provider, because providers rate-limit, deprecate models, and change pricing without warning. My routing layer is a Switch node plus a small policy table stored in a Postgres row that a 'Get Model Policy' HTTP node reads on each run. The table maps task types (classification, extraction, long writing, reasoning, bulk) to a primary model, a fallback, max tokens, and temperature, for example Claude Haiku with GPT-4o-mini as fallback for classification. The core rule is that no primary model is ever called without a defined fallback, so a provider outage becomes a degraded response instead of a failed run.
What is a reliable reference architecture for a production AI agent in n8n?
Every production n8n agent I ship follows the same six-block skeleton: a webhook trigger with auth and idempotency, a validate-and-normalize step, a router that dispatches by intent or type to one of several agent branches (each with its own LLM, tools, and memory), post-processing and guardrails, a hand-off to a queue, database, or downstream API, and a notify step for Slack or email alerts. Alongside that main flow I always add a separate Error Trigger workflow that routes failures to a dead-letter queue and sends an alert. Once you have this skeleton in place, you stop rewriting the boring parts and every new agent becomes a variation on the same pattern.
Building something hard with AI or automation? I am open to talk.
Get in touch