Grimoire 004 · n8n 101 · Free · No Gate · 2026 Edition
Narrative + Technical · 7 Parts · 25 Modules
Grimoire 004 · Knowledge System · May 2026

n8n 101: AI Workflow Automation

This isn't a reference doc I wrote once and forgot. It's the accumulated scar tissue from building real automations across real clients — the failures that cost money, the fixes that saved weekends, and the configs that actually work in production. 25 modules. Narrative first. Technical second. No paywalls.

7 Parts
25 Modules
1,200+ integrations
Self-host free tier
Fair-code license
2026 AI-native
Free · no gate
WHY
I started using n8n because Zapier billed me $800 in one month
Fair-code, self-hostable, no per-execution fees — and what that actually means
A client's lead form went viral on Facebook. Zapier processed 45,000 tasks in three days. My bill was $847. That's when I understood the structural problem with per-execution pricing: it punishes success. n8n self-hosted costs the same whether you run 100 workflows or 100,000. That's not just cheaper — it's a fundamentally different business model.

n8n is fair-code licensed (source-available, self-hostable). The license allows unlimited self-hosting with no per-execution fees. You can run it on a $6/month VPS and process as many workflows as your server can handle. The tradeoff: you maintain the server.

What "fair-code" actually means

n8n uses the Sustainable Use License for hosted/commercial use — you can't build a competing product on top of it. But for internal use, client work, and self-hosted deployments, it's effectively open source. Read the license at n8n.io/legal before building commercial products on top of it.

The numbers that matter in 2026
1,200+
Pre-built integrations (nodes)
$0
Per-execution cost (self-hosted)
Unlimited
Workflows, runs, users
Visual
Low-code + full JS/Python access
AI-native
Agent nodes, RAG, vector stores
Active
~120k GitHub stars, growing

n8n vs the alternatives — the honest positioning

Zapier is better for non-technical teams that need quick, simple integrations and don't expect volume. Make sits in the middle — visual, credit-based, no self-host. n8n is for technical operators who want control, need volume, or are building complex AI workflows. If you're reading this Grimoire, n8n is probably for you.

↺ What I learned the hard way

Don't build client systems on a pricing model that punishes success. n8n inverts that equation. The only limit is your server — which you control. The only ongoing cost is the VPS — which doesn't scale with usage. Once you internalize this, you start building bigger, bolder automations. Because the cost of running them is the same as the cost of not running them.

⚠️
CVE-2026-21858 CVSS 10.0
Unauthenticated file read — patch immediately, then rotate everything
🚨
Critical severity. Patch first, read the rest later. CVE-2026-21858 allows unauthenticated attackers to read arbitrary files from your n8n server. That includes your .env file — which contains every API key you've stored in n8n. GHL, OpenAI, Stripe, SMTP, everything. Fixed in v1.121.0+. Check your version now.
Check your version
# Check running version
docker inspect n8n | grep -i image
# Or visit: https://your-n8n.com/rest/settings

# Upgrade (Docker)
docker pull n8nio/n8n:1.121.0
docker-compose down && docker-compose up -d
⛑️ Full post-patch checklist

1. Pull and restart: docker pull n8nio/n8n:1.121.0 + docker-compose up -d
2. Rotate every credential stored in n8n — GHL, OpenAI, SMTP, Stripe, webhooks, OAuth tokens.
3. Add N8N_BLOCK_NODE_ACCESS=true to your .env (blocks filesystem node access).
4. Put n8n behind a reverse proxy (Nginx/Caddy). Never expose port 5678 directly.
5. Add IP allowlisting for admin routes (/rest/, /settings) in your proxy config.
6. Set N8N_SECURE_COOKIE=true and N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true.
7. Enable daily automated database backups. You need them before an incident, not after.
8. Audit your webhook URLs — remove any with no authentication.

Nginx reverse proxy config (hardened)

# /etc/nginx/sites-available/n8n
server {
  listen 443 ssl;
  server_name n8n.yourdomain.com;

  # Block admin REST API from non-whitelisted IPs
  location ~ ^/rest/(settings|users|credentials) {
    allow 203.0.113.10;   # your office IP
    deny all;
    proxy_pass http://localhost:5678;
  }

  # Webhooks — public
  location /webhook/ {
    proxy_pass http://localhost:5678;
    proxy_set_header X-Real-IP $remote_addr;
  }

  # Editor UI — auth required (handled by n8n)
  location / {
    proxy_pass http://localhost:5678;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
  }
}
Part I
Fundamentals
Installation, architecture, and the mental model you need before writing a single node
M1 — M4
M1
Self-hosting n8n — the complete setup
Docker, PostgreSQL, Nginx, SSL, backups, and why SQLite will betray you
I started with SQLite because it was easy. Two months later, the database corrupted during a power outage. I lost 47 workflows and three weeks of execution logs. A client was mid-campaign. PostgreSQL is not optional for production. This is not a recommendation — it's a prerequisite.

Prerequisites

Minimum Server

2 vCPU, 2GB RAM

DigitalOcean Droplet or Hetzner CX21. Hetzner is half the price for equivalent specs. I run 4 clients on a $12/mo server.

Required Software

Docker + Compose v2

apt install docker.io docker-compose-plugin on Ubuntu 22.04. Compose v2 uses docker compose (no hyphen).

DNS

A record → server IP

n8n.yourdomain.com → your VPS IP. SSL needs DNS propagation (5–30 min). Do this first before installing anything.

Ports

80, 443, 22 open

Block port 5678 from public access. n8n runs internally; Nginx proxies it to 443. Never expose 5678 directly.

Production docker-compose.yml

docker-compose.yml — production-ready
version: '3.8'

services:
  n8n:
    image: n8nio/n8n:1.121.0         # pin version, never use 'latest' in prod
    container_name: n8n
    restart: unless-stopped
    ports:
      - "127.0.0.1:5678:5678"          # bind to localhost only
    environment:
      - N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
      - N8N_HOST=${N8N_HOST}
      - N8N_PORT=5678
      - N8N_PROTOCOL=https
      - WEBHOOK_URL=https://${N8N_HOST}
      - N8N_SECURE_COOKIE=true
      - N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
      - N8N_BLOCK_NODE_ACCESS=true
      - DB_TYPE=postgresdb
      - DB_POSTGRESDB_HOST=postgres
      - DB_POSTGRESDB_PORT=5432
      - DB_POSTGRESDB_DATABASE=n8n
      - DB_POSTGRESDB_USER=n8n
      - DB_POSTGRESDB_PASSWORD=${DB_PASSWORD}
      - EXECUTIONS_DATA_PRUNE=true
      - EXECUTIONS_DATA_MAX_AGE=720        # 30 days of logs
      - GENERIC_TIMEZONE=Europe/London
    volumes:
      - ./n8n_data:/home/node/.n8n
    depends_on:
      postgres:
        condition: service_healthy

  postgres:
    image: postgres:15-alpine
    container_name: n8n_postgres
    restart: unless-stopped
    environment:
      - POSTGRES_DB=n8n
      - POSTGRES_USER=n8n
      - POSTGRES_PASSWORD=${DB_PASSWORD}
    volumes:
      - ./postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U n8n"]
      interval: 10s
      timeout: 5s
      retries: 5

Automated backups

backup.sh — runs daily via cron
#!/bin/bash
# crontab -e → 0 3 * * * /root/backup.sh

DATE=$(date +%Y%m%d-%H%M)
BACKUP_DIR=/root/backups
mkdir -p $BACKUP_DIR

docker exec n8n_postgres pg_dump -U n8n n8n > $BACKUP_DIR/n8n-db-$DATE.sql
tar -czf $BACKUP_DIR/n8n-data-$DATE.tar.gz /root/n8n_data
aws s3 cp $BACKUP_DIR/n8n-db-$DATE.sql s3://your-bucket/n8n-backups/

find $BACKUP_DIR -name "*.sql" -mtime +14 -delete
find $BACKUP_DIR -name "*.tar.gz" -mtime +14 -delete
⚡ Caddy is easier than Nginx for SSL

If you're not comfortable with Nginx, use Caddy. Caddyfile: just write n8n.yourdomain.com { reverse_proxy localhost:5678 } and Caddy handles SSL automatically via Let's Encrypt. No certbot, no renewal crons.

↺ What I'd do differently

Start with PostgreSQL from day one, even for local testing. The migration path from SQLite to Postgres is painful — export workflows, rebuild the database, re-enter credentials manually. It's a half-day of work. Postgres from the start costs nothing extra. SQLite is a trap.

M2
Cloud vs Self-host — how to actually decide
The answer depends on volume, technical comfort, and data sensitivity

n8n offers three deployment options. The decision matrix is more nuanced than "cloud is easy, self-host is hard."

n8n Cloud

$20/mo starter

No server maintenance
Automatic updates + security patches
Managed backups
Instant setup (5 minutes)
Per-workflow execution limits
Data leaves your infrastructure
More expensive at scale

Self-Hosted (VPS)

$6–20/mo server

Unlimited executions, no throttling
Full data control (GDPR easier)
Custom env variables, filesystem access
Community nodes installable
You manage updates + security
You manage backups
Requires basic Linux / Docker

Self-Hosted (Enterprise)

Custom pricing

SSO / SAML / LDAP
Version control (git)
Audit logs
External secrets (Vault, AWS Secrets)
Sales process required
Overkill for small teams
Choose Cloud if...
You're just starting out, don't want to manage infra, or your volume stays under the Cloud plan limits (~2,500 runs/mo on Starter).
Choose Self-host if...
Your volume is high (5,000+ runs/mo), you're handling sensitive client data, you need community nodes, or you want zero per-execution cost.
Migration path
Start on Cloud → export workflows as JSON when ready → re-create credentials on self-hosted instance. Takes 2–4 hours depending on credential count.
⚡ My recommendation for new builders

Start on n8n Cloud. Learn the tool. Build your first five workflows. Then, when you understand your volume and complexity needs, decide whether self-hosting is worth the ops overhead. Most people never need to self-host. Some absolutely do. Know which you are before committing.

M3
The Editor UI — anatomy and keyboard shortcuts
What everything is called and how to stop clicking so much

The five zones you live in

Canvas

The workflow graph

Nodes are the boxes. Connections are the arrows. Click + drag to pan. Scroll to zoom. Cmd/Ctrl+A to select all. Double-click empty canvas to add a node.

Node Panel

Left sidebar

Search for nodes. Filter by category (AI, Core, Communication). Drag to canvas or press Enter on a highlighted node. Cmd/Ctrl+K opens search from anywhere.

Node Editor

Right panel

Click any node to configure it. Parameters, credentials, output preview. Tab between fields. Click the lightning bolt to test the node in isolation.

Executions

Top-right menu

Full log of past runs. Click any execution to see the exact data at each node. Essential for debugging. Filter by workflow, status, date.

Keyboard shortcuts worth memorizing

ShortcutActionWhy it matters
Cmd/Ctrl + KNode searchFastest way to add any node without touching the sidebar
Cmd/Ctrl + EnterExecute workflowTest without leaving the canvas
TabMove to next fieldNavigate node editor without mouse
Ctrl + Shift + DDuplicate nodeClone config for similar nodes
F2Rename nodeName nodes descriptively — essential for complex flows
Cmd/Ctrl + ASelect all nodesMove entire workflow at once
Cmd/Ctrl + Z / YUndo / Redo20 undo levels available
Space + dragPan canvasFaster than scrollbars on large workflows
Cmd/Ctrl + \Toggle sidebarMore canvas space when debugging

Workflow organization

Naming convention
[category] action — description e.g. [lead] D0 — welcome email + GHL tag
Tags
Use tags to group workflows by client, project, or type. Filter in the workflows list. Saves hours when you have 80+ workflows.
Sticky notes
Add sticky notes to explain non-obvious logic. Right-click canvas → Add sticky note.
Node naming
Name every node. "HTTP Request3" tells you nothing. "Create GHL Contact" tells you everything. Press F2 to rename.
One workflow, one job
The cardinal rule. Build small, focused workflows and chain them via sub-workflows (M10).
⚡ The execution data inspector

After running a workflow, click any node on the canvas to see the exact input/output data for that node. This is the fastest debugging tool in n8n — faster than console.log, faster than writing to Sheets. Learn to read the JSON output panel.

M4
The n8n data model & expressions
The mental model that explains everything — items, nodes, the execution context
I spent two weeks building n8n workflows without understanding what $json actually referred to. I thought it was "the data." It is — but it's the data of the current item, from the previous node. Once I understood items and the execution context properly, every confusing behavior suddenly made sense.

The item model

n8n passes data between nodes as an array of items. Each item has a json property (your data) and optionally a binary property (files). Understanding this is fundamental.

What a node receives and emits
// A node receives items — an array of objects:
[
  { "json": { "email": "alice@co.com", "score": 85 } },
  { "json": { "email": "bob@co.com",   "score": 42 } }
]

// By default, most nodes process each item independently (1-in, 1-out)
// Some nodes merge items (Summarize, Aggregate)
// Some nodes split items (Split Out, SplitInBatches)

The expression context variables

Expressions use {{ }} mustache syntax. Inside expressions, you have access to these variables:

Expression
What it returns
$json
The json property of the current item from the previous node. Equivalent to $input.item.json.
$json.fieldName
A specific field. Handles dot notation for nested objects: $json.contact.email
$input.all()
All items from the previous node as an array. Use in Code node to process all at once.
$input.first()
The first item from the previous node. Useful when you know you only want one result.
$node["Node Name"].json
Access the output of any named node — not just the immediately previous one.
$env.VARIABLE_NAME
Environment variable. Store API keys, secrets, and config here. Never hardcode.
$workflow.name
Name of the current workflow. Use in error alert messages.
$execution.id
Unique ID for this execution run. Use for deduplication and tracing.
$execution.mode
manual (test run) or trigger (production). Use to skip side effects in test mode.
$now
Current timestamp as a Luxon DateTime object. $now.toISO(), $now.toFormat('yyyy-MM-dd'), etc.
$today
Today's date at midnight UTC. Shorthand for $now.startOf('day').
$itemIndex
Zero-based index of the current item in the input array.

Common expression patterns

// String interpolation
Hello {{ $json.firstName }}, your booking for {{ $json.propertyAddress }} is confirmed.

// Conditional value
{{ $json.budget >= 2000 ? 'HOT' : 'WARM' }}

// Date formatting
{{ $now.toFormat('dd MMM yyyy') }}          // "14 May 2026"
{{ $now.minus({days: 7}).toISO() }}         // 7 days ago

// Null coalescing
{{ $json.phone || 'no phone provided' }}

// Access upstream node data
{{ $node["Webhook"].json.body.email }}

// Skip side effects in test mode
{{ $execution.mode === 'manual' ? 'test@example.com' : $json.email }}

The Code node — when expressions aren't enough

// Code node runs JS. Access all items:
const items = $input.all();

return items.map(item => {
  const { email, budget, move_in_date } = item.json;

  const daysUntil = Math.ceil(
    (new Date(move_in_date) - new Date()) / (1000 * 60 * 60 * 24)
  );

  let score = 'COLD';
  if (budget >= 2000 && daysUntil <= 30) score = 'HOT';
  else if (budget >= 1500 || daysUntil <= 60) score = 'WARM';

  return {
    json: { ...item.json, score, days_until_move_in: daysUntil }
  };
});
ℹ️
Python support: n8n 1.0+ supports Python in the Code node. Switch the language dropdown. Access items the same way: items = _input.all().
Part II
Core Nodes
The building blocks that every real workflow is made of
M5 — M10
M5
Webhooks — the front door of every automation
Authentication, idempotency, test vs production URLs, and the scare that changed how I build
A client's webhook endpoint was publicly exposed for three weeks with no authentication. I didn't know until someone in Romania started POSTing test data. No damage — the workflow only created test contacts in a sandbox. But I spent an anxious hour auditing every workflow. Now every webhook has authentication on day one.
⚠️
Test vs Production URLs. The test URL (/webhook-test/[path]) only works when the editor is open and you've clicked "Listen for test event." The production URL (/webhook/[path]) works when the workflow is active. Always test with the test URL, then switch the sending system to the production URL.
Webhook Node — Production Config POST
HTTP Method
POST
Path
lead-inbound-{{ $env.WEBHOOK_PATH_SUFFIX }}
Authentication
Header Auth → Name: X-Webhook-Token → Value: {{ $env.WEBHOOK_SECRET }}
Respond
Immediately (respond 200 first, then process async)
Response Body
{"received": true, "id": "{{ $execution.id }}"}

Webhook authentication options

MethodSecurity LevelWhen to Use
None🔴 NoneTesting only, localhost — never production
Header Auth🟡 GoodCustom integrations, server-to-server
Basic Auth🟡 GoodLegacy systems, some CMS
JWT🟢 StrongUser-facing webhooks, mobile apps
Signature Verify🟢 StrongStripe, GitHub, Shopify webhooks — verify HMAC in Code node

Idempotency — the problem most people ignore until it's too late

A Typeform webhook fired twice for the same submission (Typeform's retry behavior on timeout). We created two contacts in GHL for the same person. One went through the hot-lead sequence, one sat cold. The lead was confused, the sales rep was confused. Fixed with idempotency — check before you create.
Idempotency check pattern
// After webhook, before creating records — check if already processed

HTTP Request: GET Sheets — check Processed!A:A for submission token

Code node:
const processed = $input.first().json.values?.flat() || [];
const submissionId = $node["Webhook"].json.body.token;

if (processed.includes(submissionId)) {
  return [];  // empty array stops execution — duplicate
}
return [{ json: { submission_id: submissionId, ...$node["Webhook"].json.body } }];
⚡ "Respond Immediately" vs "Last Node"

Always set webhooks to "Respond Immediately" for production integrations. Returns HTTP 200 instantly, then processes async. If you use "Last Node," the calling system waits for your entire workflow — which can timeout for long-running AI workflows. Typeform, Stripe, and Facebook all have short timeout windows.

M6
HTTP Request — the universal connector
When no pre-built node exists, this reaches everything. Pagination, auth, retry, file handling.

If a service has a REST API, the HTTP Request node can integrate with it. GHL, custom CRMs, internal tools, legacy systems — anything with an API endpoint. This is the most powerful node in n8n after the Code node.

Authentication patterns

Auth TypeConfigCommon APIs
Bearer TokenAuthorization: Bearer {{ $env.API_KEY }}GHL, OpenAI, most modern REST APIs
API Key (header)Custom header e.g. X-API-Key: {{ $env.KEY }}Airtable, SendGrid, various
Basic AuthUsername + password in CredentialsLegacy systems, WooCommerce
OAuth2Set up credential once, n8n handles refreshGoogle APIs, Salesforce, HubSpot
Custom (Code node)Sign request in Code before HTTP nodeAWS (SigV4), Stripe (HMAC)

Pagination

HTTP Request node — cursor-based pagination:
Pagination Type: Response Contains Next URL
Next URL Expression: {{ $response.body.meta.nextPageUrl }}

// Or for offset-based:
Pagination Type: Update a Parameter
Parameter Name: offset
Value: += 100
Max Pages: 50  (safety limit)
Stop When: response returns empty array
HTTP Request — Retry Settings
Retry on Fail
✓ Enabled — 3 attempts, exponential backoff (1s, 2s, 4s)
Continue on Fail
Off for critical ops (GHL create, payments). On for non-critical (analytics, logging).
↺ A mistake I kept making

I used to hardcode API keys in HTTP Request node configuration. Then I'd export a workflow to share with a client — and my keys went with it. Now every secret lives in $env.VARIABLE_NAME. Workflow JSONs can be safely shared and version controlled. The secrets stay in .env on the server.

M7
Data transformation nodes
Set, Edit Fields, Item Lists, Merge, Aggregate — the toolkit for reshaping data
Set / Edit Fields

Add, rename, remove fields

The most-used transformation node. Add calculated fields, rename API response fields to your schema, remove sensitive fields before logging.

Item Lists

Merge, split, aggregate

Split a field that contains an array into separate items. Or merge multiple items into one. Essential for API responses that return arrays.

Merge

Combine data streams

Join two branches of data by a key (like SQL JOIN), wait for both branches, or choose between branches.

Aggregate

Combine all items into one

Collapse N items into 1 with summary statistics. Use for report generation: all leads → {total: N, hot: M}.

Merge node — the four modes

ModeWhat it doesUse case
Merge by IndexPairs item 1 from A with item 1 from BWhen two branches process the same items in parallel
Merge by KeyJoins by matching field value (like SQL JOIN)Enrich lead data from two different API calls using email as key
AppendConcatenates all items from A and BCombine results from two searches into one result set
WaitWaits for both branches to completeWhen you need both A and B to finish before the next step
Data normalization example
// Incoming webhook (raw, messy):
{ "lead_first_name": "Alice", "budget_pcm": "£2,500", "properties": ["flat","2-bed"] }

// Edit Fields node — reshape to schema:
fullName      = {{ $json.lead_first_name + ' ' + $json.lead_last_name }}
budget        = {{ parseInt($json.budget_pcm.replace(/[^0-9]/g, '')) }}
property_type = {{ $json.properties.join(', ') }}

// Output:
{ "fullName": "Alice Chen", "budget": 2500, "property_type": "flat, 2-bed" }
M8
Logic & control flow — IF, Switch, Wait, Loop, Filter
How to stop building spaghetti workflows
I once built a workflow with 47 nodes. It handled everything — lead scoring, CRM sync, email, WhatsApp, Slack alerts, reporting. When something broke, finding the broken node took 40 minutes. Now I follow one rule: one workflow, one job.
IF Node

Two-way branch

True/False paths. Conditions: equal, not equal, contains, regex match, empty, exists. Stack multiple conditions with AND/OR.

Switch Node

Multi-way branch

Route items to N outputs based on a field value. Perfect for: residential→team1, commercial→team2, industrial→team3.

Wait Node

Delay or pause

Wait fixed time, until a datetime, or until a webhook resumes the execution. Essential for drip sequences.

Filter Node

Remove items

Discard items that don't meet a condition. Unlike IF, this doesn't branch — it just drops unwanted items and continues.

SplitInBatches

Loop over batches

Process 100 items at a time instead of all at once. Prevents API rate limit errors.

Execute Workflow

Call sub-workflow

Trigger another workflow and optionally wait for its result. The key to modular architecture. See M10.

⚡ Wait node gotcha — server restarts

If your n8n server restarts while a workflow is paused at a Wait node, that execution will continue from where it stopped — because Wait state is persisted to the database. This is why PostgreSQL is non-negotiable for drip sequences. SQLite can corrupt on restart and lose your paused executions.

M9
Error handling — the thing you forget until it breaks at 3am
Error Trigger, retry logic, dead letter queues, silent failure detection
A GHL API rate limit error started failing at 2:13am. The workflow retried once (default), failed silently, and stopped. No alert. No notification. By 9am, 147 leads had not been synced to CRM. The client found out when a hot lead called asking why they hadn't heard back. That was the last time I deployed a workflow without global error handling.

The Error Trigger workflow — build this before anything else

Global error handler workflow
// Workflow name: [system] global error handler

Trigger: Error Trigger  ← under "Core" in node panelCode node — format alert:
const err = $input.first().json;
return [{
  json: {
    workflow: err.workflow.name,
    error: err.execution.error?.message,
    url: `https://your-n8n.com/workflow/${err.workflow.id}/executions/${err.execution.id}`
  }
}];
  ↓
Slack node — post to #automation-alerts:
🚨 *{{ $json.workflow }}* failed: {{ $json.error }}
  ↓
Google Sheets append — dead letter log:
Timestamp | Workflow | Error | URL | Resolved
ℹ️
How to connect it: Open any other workflow → Settings → Error Workflow → select "[system] global error handler". Set this on every workflow, or use the n8n API to bulk-set it (see M22).
SettingBehaviorUse when
Retry on Fail: OnRetries N times before failingAlmost always. Transient network errors are common.
Continue on Fail: OffWorkflow stops, triggers Error WorkflowCritical operations (payment, CRM create)
Continue on Fail: OnError data passes to next node, workflow continuesNon-critical (analytics, logging, enrichment)
⚡ Monitor your error log weekly

A dead letter queue only helps if someone reads it. Add a Monday 9am scheduled workflow that counts PENDING rows in your dead letter Sheets tab. If > 0, post to Slack: "🟡 Dead letter queue: 4 items need review." This turns error tracking from reactive to proactive.

M10
Sub-workflows — modular architecture for maintainable automation
One workflow, one job. How to structure 50 workflows so nothing is a mess.
Modular workflow architecture — property management example
// ENTRY POINT WORKFLOWS (triggered by external events)
[lead] webhook — GHL form            → calls → [lead] process new lead
[lead] webhook — WhatsApp inbound    → calls → [lead] process new lead

// CORE WORKFLOW (the one job)
[lead] process new lead
  ↓ calls → [util] create GHL contact
  ↓ calls → [util] send welcome email
  ↓ IF hot → calls → [alert] slack hot lead
  ↓ calls → [lead] start drip sequence

// UTILITY WORKFLOWS (reusable)
[util] create GHL contact           ← called from: 3 entry points
[util] send welcome email           ← called from: 2 workflows

// SCHEDULED WORKFLOWS
[report] weekly KPI summary         → every Monday 09:00
[maint] check dead letter queue     → every Monday 09:05

Execute Workflow node — sync vs async

ModeBehaviorUse when
Wait for resultPauses until sub-workflow returns dataYou need the sub-workflow's output to continue
Fire and forgetTriggers sub-workflow and immediately continuesSide effects that don't affect the main flow
↺ The naming convention that saved my sanity

Every workflow gets a prefix: [lead], [util], [report], [alert], [maint]. Filter the workflow list by prefix. When debugging you immediately know what category you're in. When onboarding someone new, they understand the architecture before opening a single workflow.

006
Know the pattern before you build the workflow. Grimoire 006 (Automations 101) covers 25 production patterns that map directly to what you’re about to build in n8n. The architecture decisions in G006 determine whether your workflows are maintainable three months from now or a debugging nightmare.
Part III
AI Workflows
Agents, RAG, classification, and the production patterns that actually work
M11 — M16
M11
The AI Agent node — how it actually works
Tools, memory, reasoning loops, and when to use agents vs simple LLM calls

The AI Agent node is n8n's most powerful AI feature. Unlike a simple LLM call (question → answer), an AI Agent can use tools — nodes it can call to take actions — and reason over multiple steps before returning a final answer.

Use LLM Node when...

Simple transformation or generation

Classify a lead, write an email, extract fields from text. One input → one output. Faster and cheaper than an agent.

Use AI Agent when...

Multi-step reasoning with actions

Answer a support question by searching a knowledge base AND checking a CRM AND maybe sending an email. The agent decides which tools to use.

The reasoning loop

1

User message → Agent

Agent receives the input, plus its system prompt and available tools.

2

LLM decides next action

The model reasons: call a tool, call multiple tools, or respond directly.

3

Tool executes

n8n runs the tool (HTTP Request, vector search, calculator) and returns the result.

4

Repeat or respond

If the model needs more information, it calls another tool. If it has enough, it responds. Max iterations prevent infinite loops.

AI Agent Node — Full Config
Chat Model
OpenAI GPT-4o-mini (fast + cheap) or GPT-4o (complex reasoning)
System Prompt
Define role, constraints, output format. Explicit about what NOT to do.
Max Iterations
5 (increase to 10 for complex tasks; cap prevents runaway tool calls)
Memory
Window Buffer (last N messages) or Redis-backed for persistent sessions
Tools
Any n8n node: HTTP Request, vector search, calculator, code node
⚡ System prompt engineering for agents

Good agent system prompts have four parts: (1) Role definition, (2) Constraints ("Only answer using the knowledge base tool"), (3) Escalation rule ("If confidence < 0.7, escalate to human"), (4) Output format ("Always respond in under 200 words"). Without explicit constraints, the agent will improvise — fine in demos, dangerous in production.

007
The AI Agent node is one step away from a full agentic system. Grimoire 007 (Agentic AI 101) covers what happens when your n8n agent needs to grow — multi-agent systems, supervisor design, observability, evals, and when to reach for a dedicated framework instead.
M12
RAG pipelines — giving your AI a knowledge base
Ingestion, chunking, embedding, retrieval — the full pipeline built in n8n

RAG (Retrieval-Augmented Generation) means: before answering, search a knowledge base for relevant context, then give that context to the LLM along with the question. The LLM answers based on your data, not just its training data. This is how you build AI that knows your business.

Workflow 1 — Knowledge base ingestion
Trigger: Manual / Schedule
  ↓
Google Drive node: Get all docs from /knowledge-base folder
  ↓
Text Splitter node:
  Chunk Size: 800 tokens
  Chunk Overlap: 80 tokens (10% overlap retains context across boundaries)
  Splitter: Recursive Character
  ↓
Embeddings node: text-embedding-3-small
  ↓
Vector Store (Pinecone / Qdrant):
  Operation: Upsert
  Metadata: { source, doc_title, chunk_index, last_updated }
Workflow 2 — Retrieval (runs per user query)
Input: { "query": "How do I submit a maintenance request?" }
  ↓
Embeddings node: embed the query text
  ↓
Vector Store node:
  Operation: Search | Top K: 5 | Score Threshold: 0.72
  ↓
Code node: format context
const context = $input.all()
  .filter(c => c.json.score >= 0.72)
  .map((c, i) => `[${i+1}] ${c.json.text}`)
  .join('\n\n');
  ↓
LLM node:
  System: Answer ONLY using the provided context. If not in context,
          say "I need to check on that" and return confidence: 0.3.
  User: Context:\n{{ $json.context }}\n\nQuestion: {{ $json.query }}
Chunk too large (>1500 tokens)
Too much noise. LLM gets confused by irrelevant parts. Score looks high but answer quality suffers.
Chunk too small (<200 tokens)
Not enough context. Answers miss nuance. Requires retrieving more chunks for full picture.
Sweet spot: 600–900 tokens
Enough context for coherent answers. Small enough for accurate similarity matching. Use 10% overlap.
↺ The feedback loop I should have built first

When a human answers an escalated question, that Q&A pair is gold. Feed every escalated Q&A back into the knowledge base as a new chunk. Your bot gets smarter from its own failures. Without this loop, your bot never improves. I added it three months late. The improvement was immediate.

M13
AI customer support bot — end to end
How I replaced repetitive support work for a property management client
A property management client had 200+ support messages daily. Tenants asked the same questions over and over. Support staff typed the same answers, hour after hour. I watched one person paste "Rent is due on the 1st" fourteen times in a single morning. Two months later, the bot handles 70% of questions instantly.
Full workflow — Slack support bot with RAG + escalation
Step 1: Webhook — receive Slack event
  Auth: Slack request signature verification (HMAC)
  Respond: Immediately (Slack has 3s timeout)

Step 2: Code node — extract message
const text = payload.event.text.replace(/<@[^>]+>/g, '').trim();
return [{ json: { userId: payload.event.user, text, channel: payload.event.channel } }];

Step 3: Vector Search (Qdrant)
  Top K: 5 | Score Threshold: 0.70

Step 4: AI Agent
  Model: gpt-4o-mini (temperature: 0.2)
  Memory: PostgreSQL (keyed by userId)
  System:
    You are a support agent for Whitmore Property Management (UK).
    Answer ONLY using the provided knowledge base context.
    Be concise — max 150 words. Use UK English.
    If answer not in context, set confidence = 0.2 and say:
    "I'd like to check this with the team."

Step 5: IF — confidence >= 0.65?
  TRUE  → Send AI reply to user via Slack DM
  FALSE → Post to #support-escalations for human review

Step 6: Google Sheets append — log all interactions
  timestamp | user | question | response | confidence | escalated
Results after 60 days
71%
Questions answered by AI without escalation
~8s
Average response time (vs 2–4 hours before)
29%
Escalations — only the genuinely hard questions
M14
Lead qualification + CRM sync
AI scoring, GHL tagging, and the routing logic that runs while you sleep
Every form submission went into a shared email inbox. Someone had to read it, decide if the lead was hot, and manually enter it into GHL. Hot leads sat for hours. Warm leads were ignored for days. The answer wasn't better process — it was removing the human from routine decisions entirely.
Full lead qualification workflow
Step 1: Webhook
  Receives: name, email, phone, property_type, budget_pcm, move_in_date, source

Step 2: LLM Node (classification only — not an agent)
  Model: gpt-4o-mini | Temperature: 0
  System: You are a lead scoring engine. Output ONLY valid JSON.
  User:
    Score this rental lead as HOT, WARM, or COLD.
    HOT:  budget >= 2000 AND days_to_move <= 30
    WARM: budget >= 1500 OR days_to_move <= 60
    COLD: all others
    Lead: {{ JSON.stringify($json) }}
    Return: { "score": "HOT|WARM|COLD", "reason": "...", "priority_action": "..." }

Step 3: Switch node
  HOT  → GHL contact + Slack alert + WhatsApp template
  WARM → GHL contact + email sequence
  COLD → GHL contact + monthly newsletter only

Step 4: Google Sheets append
  timestamp | name | email | budget | score | reason | source
Common GHL API calls
// Create contact (v2 API)
POST https://services.leadconnectorhq.com/contacts/
Authorization: Bearer {{ $env.GHL_API_KEY }}
{
  "email": "{{ $json.email }}",
  "firstName": "{{ $json.first_name }}",
  "tags": ["{{ $json.score.toLowerCase() }}", "{{ $json.source_clean }}"],
  "locationId": "{{ $env.GHL_LOCATION_ID }}"
}

// Add to pipeline
POST https://services.leadconnectorhq.com/opportunities/
{
  "pipelineId": "{{ $env.GHL_PIPELINE_HOT }}",
  "pipelineStageId": "{{ $env.GHL_STAGE_NEW }}",
  "contactId": "{{ $json.contact_id }}"
}
M15
WhatsApp landlord lead handler
90% open rates, painful setup, incomparable ROI — the complete implementation
In the UK, WhatsApp open rates are 90%+. Email is 22%. We tested both in parallel for six weeks. Email averaged 18% open rate. WhatsApp averaged 87%. The extra setup time paid back in week two.
⚠️
WhatsApp Business API rules: First messages to new contacts must use pre-approved templates. Free-form messages only allowed within 24 hours of the contact's last inbound message. Violating this results in account suspension. Meta enforces this strictly.

Setup checklist

1

Meta Business Account

Must be verified. Requires business registration documents. Allow 3–7 days.

2

WhatsApp Business Account (WABA)

Create inside Meta Business Manager. Link a phone number (UK mobile or VoIP).

3

Message Template Approval

Submit templates to Meta. 1–3 business days. Submit 3–5 templates at once.

4

Webhook Configuration

Configure your n8n webhook URL in Meta Developer Portal.

5

Test with sandbox number

Meta provides a test number for development. Test all templates before going live.

WhatsApp inbound handler
Step 1: Webhook — receive Meta Cloud API POST
  IF $json.hub?.challenge → return challenge (text/plain)

Step 2: Extract message
const msg = $input.first().json.entry?.[0]?.changes?.[0]?.value?.messages?.[0];
if (!msg || msg.type !== 'text') return [];
return [{ json: { from: msg.from, text: msg.text.body } }];

Step 3: LLM extract — parse landlord intent
  Return JSON: { property_type, bedrooms, location, asking_rent_pcm, missing_fields }

Step 4: IF — missing required fields?
  TRUE  → Send template requesting missing info
  FALSE → Create GHL contact + send confirmation template

Send template:
POST https://graph.facebook.com/v19.0/{{ $env.WABA_PHONE_ID }}/messages
{
  "messaging_product": "whatsapp",
  "to": "{{ $json.from }}",
  "type": "template",
  "template": {
    "name": "property_details_request",
    "language": { "code": "en_GB" }
  }
}
M16
Weekly report generator — automated client reporting
Stop wasting Friday afternoons on manual reports nobody reads
Every Friday at 4pm, a senior ops person spent 2–3 hours pulling numbers from GHL, formatting them in Sheets, writing a summary email. One Friday they were sick. The report didn't go out. The client was angry Monday. Automating this took four hours to build and saved those four hours every single week indefinitely.
Full weekly report workflow
Step 1: Schedule Trigger — Every Monday at 09:00 (Europe/London)

Step 2: HTTP Request — fetch leads from GHL (last 7 days)
  createdAfter: {{ $now.minus({days:7}).toISO() }}

Step 3: Code node — compute metrics
const contacts = $input.all().map(i => i.json);
const total     = contacts.length;
const hot       = contacts.filter(c => c.tags?.includes('lead-hot')).length;
const converted = contacts.filter(c => c.tags?.includes('converted')).length;

return [{
  json: {
    period_start: new Date(Date.now()-7*86400000).toISOString().slice(0,10),
    total, hot, converted,
    conversion_rate: total ? ((converted/total)*100).toFixed(1) : 0
  }
}];

Step 4: LLM node — 3-sentence executive summary from the numbers

Step 5: Parallel branches
  A → Google Sheets append (historical log)
  B → Email to client (SMTP)
  C → Slack to #client-reports (team quick view)
↺ Email reports nobody reads

I started with email-only reports. Open rate: ~30%. Then I added the Slack preview with key metrics. Now the team sees the numbers the moment they open Slack Monday morning. Don't assume people will open their email. Put the summary where they already are.

Part IV
Channels
WhatsApp, Telegram, Slack, and email — deep configs for each
M17 — M18
M17
WhatsApp + Telegram — deep config
When to use each, and the configs that make them reliable in production
DimensionTelegramWhatsApp Business API
Setup time~15 minutes3–10 days (verification + approvals)
Template approvalNot requiredRequired for first outbound message
Open rates~45% (tech-savvy users)~90% (mainstream UK/EU audiences)
Free tierUnlimited1,000 conversations/month free
Best use caseInternal ops alerts, developer toolsCustomer-facing B2C communication

Telegram setup (complete)

# 1. Create bot via @BotFather → /newbot → get TOKEN
# 2. Get your chat ID
curl https://api.telegram.org/botTOKEN/getUpdates

# 3. Set webhook to receive messages
curl -X POST https://api.telegram.org/botTOKEN/setWebhook \
  -d url=https://your-n8n.com/webhook/telegram \
  -d secret_token=your-secret

# 4. Send message from n8n
POST https://api.telegram.org/botTOKEN/sendMessage
{
  "chat_id": "{{ $json.chat_id }}",
  "text": "Your message here",
  "parse_mode": "Markdown"
}
⚡ When to use which

Telegram for internal alerts (server down, workflow failed). WhatsApp for customer-facing communication in the UK (90%+ open rates). Don't try to use WhatsApp for internal alerts — the template approval process makes it too slow for operational needs.

M18
Slack + Email — the channels your team actually lives in
Rich Slack blocks, HTML email templates, and SMTP vs transactional API
Rich Slack Block Kit message — hot lead alert
POST https://slack.com/api/chat.postMessage
Authorization: Bearer {{ $env.SLACK_BOT_TOKEN }}
{
  "channel": "#hot-leads",
  "blocks": [
    {
      "type": "header",
      "text": { "type": "plain_text", "text": "🔥 Hot Lead — Action Required" }
    },
    {
      "type": "section",
      "fields": [
        { "type": "mrkdwn", "text": "*Name*\n{{ $json.fullName }}" },
        { "type": "mrkdwn", "text": "*Budget*\n£{{ $json.budget }}/mo" }
      ]
    },
    {
      "type": "actions",
      "elements": [{
        "type": "button",
        "style": "primary",
        "text": { "type": "plain_text", "text": "View in GHL" },
        "url": "https://app.gohighlevel.com/contacts/{{ $json.ghl_id }}"
      }]
    }
  ]
}

Email — SMTP vs transactional API

MethodSetupBest for
SMTP (Gmail/Outlook)5 min, App PasswordLow volume (<500/day), internal reports, client emails
SendGrid API15 min, API keyMarketing emails, high volume, transactional
Postmark15 min, API keyReceipts, confirmations, time-sensitive
Amazon SES30 min, IAM setup>10,000 emails/month
💡
Gmail App Password: Go to Google Account → Security → 2FA must be on → App Passwords → generate one for "n8n." Never use your main Gmail password in n8n.
Part V
Production
The unglamorous work that determines whether your automation survives contact with reality
M19 — M23
M19
Environment management & secrets
How secrets work in n8n, and why you should never export a workflow with hardcoded keys
n8n Credentials

OAuth + API auth

Managed by n8n. Encrypted with your N8N_ENCRYPTION_KEY. Used by native nodes (Slack, Google, etc). Do not export.

Environment Variables

$env.VARIABLE

Set in .env or Docker environment. Accessible in expressions. Never appear in exported JSON.

Workflow Variables

$vars.variable

Set in workflow settings. Shared across all executions. Good for client-specific config (location IDs, pipeline IDs).

.env — complete example
# ─── n8n core ───
N8N_ENCRYPTION_KEY=64-char-hex
N8N_HOST=n8n.yourdomain.com
DB_PASSWORD=strong-db-password

# ─── GHL ───
GHL_API_KEY_CLIENT_A=...
GHL_LOCATION_ID_CLIENT_A=...

# ─── AI ───
OPENAI_API_KEY=sk-...

# ─── channels ───
SLACK_BOT_TOKEN=xoxb-...
META_TOKEN=EAA...
TELEGRAM_BOT_TOKEN=...

# ─── email ───
SMTP_USER=ops@yourdomain.com
SMTP_PASS=gmail-app-password
Export workflow JSON
Workflows export as JSON (no credentials or env vars). Safe to commit to git.
.gitignore
Add: .env, n8n_data/, postgres_data/
Multi-environment
Run separate n8n instances for dev/staging/prod. Different .env files. Never test against production data.
M20
Queue mode & scaling
When single-instance isn't enough — Redis, workers, and concurrency limits
ℹ️
Most people never need this. Queue mode is for high-concurrency scenarios: 100+ simultaneous executions, webhook spikes, or long-running AI workflows that block the main thread. If you're running under 50 concurrent executions, default mode is fine.
docker-compose.yml — queue mode with workers
services:
  n8n-main:              # handles UI + webhook reception
    image: n8nio/n8n:1.121.0
    environment:
      - EXECUTIONS_MODE=queue
      - QUEUE_BULL_REDIS_HOST=redis

  n8n-worker:           # handles workflow execution
    image: n8nio/n8n:1.121.0
    command: n8n worker --concurrency=10
    deploy:
      replicas: 3           # 3 workers × 10 = 30 parallel executions

  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes
Worker concurrency
--concurrency=N — workflows per worker simultaneously. Default: 10. Memory-heavy AI workflows: use 3–5.
Workflow-level limit
Set in workflow settings → "Max concurrent executions." Prevents one high-volume workflow from saturating all workers.
M21
Testing workflows — the discipline that prevents 3am incidents
Manual testing, mock data, staging environments, and the pre-deploy checklist
1

Node-level test

Click the test button (lightning bolt) on an individual node with mock input. Verify output before connecting downstream.

2

Partial workflow test

Right-click any node → "Test from here" — runs from that node forward using real or mock data.

3

Full workflow test

Click "Test Workflow" button. Uses real credentials, real APIs. Verify in staging/test accounts.

4

Test mode isolation

Use {{ $execution.mode === 'manual' ? 'test@...' : $json.email }} to redirect real side-effects during testing.

Pre-deploy checklist

Error handler set
Workflow Settings → Error Workflow → "[system] global error handler" ✓
Retry enabled
All HTTP Request nodes: Retry on Fail = On, Max 3 attempts ✓
No hardcoded keys
Search workflow JSON for "sk-", "EAA", "Bearer" — should be zero results ✓
Webhook authenticated
All webhook nodes have authentication configured ✓
All paths tested
Every branch of every IF/Switch node has been executed at least once ✓
Node names meaningful
No "HTTP Request3" or "IF5" — all nodes have descriptive names ✓
M22
The n8n API — managing your instance programmatically
Bulk-configure error handlers, export workflows to git, monitor execution health
Common n8n API calls
// Auth: X-N8N-API-KEY header (create in Settings → n8n API)

// List all workflows
GET /api/v1/workflows

// Export workflow JSON (safe to git-commit)
GET /api/v1/workflows/{id}

// Bulk-set error workflow on all workflows
PATCH /api/v1/workflows/{id}
{ "settings": { "errorWorkflow": "error-handler-workflow-id" } }

// Get all failed executions for a workflow
GET /api/v1/executions?workflowId={id}&status=error&limit=20

// Trigger workflow via API
POST /api/v1/workflows/{id}/execute
{ "runData": { "Start": [{ "json": { "your": "data" } }] } }

Auto-backup workflow (Sunday 2am)

Trigger: Schedule → Sunday 02:00
  ↓
HTTP Request: GET /api/v1/workflows
  ↓
SplitInBatches: size 1
  ↓
HTTP Request: GET /api/v1/workflows/{{ $json.id }}
  ↓
GitHub node: create/update file
  Path: workflows/{{ $json.name.replace(/[^a-z0-9]/gi,'-').toLowerCase() }}.json
  Commit: "Auto-backup {{ $json.name }} — {{ $now.toFormat('yyyy-MM-dd') }}"
M23
Community nodes — what's actually useful
How to install them (self-hosted only), quality signals, and the ones I actually use
⚠️
Self-hosted only. Community nodes cannot be installed on n8n Cloud. Also: community nodes are third-party code running on your server. Review the source before installing on production.
# Via n8n UI (easiest)
Settings → Community Nodes → Install → enter npm package name

# Via CLI (inside Docker container)
docker exec -it n8n n8n nodes install n8n-nodes-packagename
PackageWhat it doesQuality signal
n8n-nodes-browserlessBrowser automation (scraping, PDF generation)High downloads, active maintenance
n8n-nodes-mcpConnect to MCP servers from n8n workflowsGrowing, watch actively
n8n-nodes-redisDirect Redis operations — rate limiting, session storageStable, widely used
n8n-nodes-document-generatorGenerate PDF/DOCX from HTML templatesActive, good docs
⚡ The quality filter

Before installing: (1) Check npm last-publish date — anything over 12 months without updates is risky. (2) Check GitHub issues — are critical bugs open? (3) Check if it's been updated for n8n 1.0+ API. When in doubt, use the HTTP Request node — it's always maintained.

Part VI
Reference
Comparison tables, expression cheatsheet, and cited sources
M24 — M25
M24
n8n vs Make vs Zapier (2026) — from someone who uses all three
The honest comparison, including the situations where n8n is the wrong choice
Dimensionn8nMakeZapier
Pricing modelSelf-host (server cost) or $20/mo CloudCredit-based (~$9–$29/mo)Task-based ($20–$800+/mo)
Per-execution costNone (self-host)Yes (credits consumed)Yes (tasks consumed)
Self-host option✓ Yes, Docker✗ No✗ No
Native integrations1,200+3,000+6,000+
Code supportFull JS + PythonLimited (basic functions)Limited (Code by Zapier)
AI featuresAI Agent, RAG, vector stores, LLM nodeMake AI Agents (improving)Zapier AI (GPT-based, basic)
Community nodes✓ npm ecosystem
Learning curveSteep (technical comfort needed)ModerateEasy (non-technical friendly)
Error handlingFull — Error Trigger, retry, dead letterGood — built inBasic — email alerts
Best forHigh-volume, AI workflows, technical teamsMarketing automation, visual buildersNon-technical teams, quick simple integrations
Worst forNon-technical teams, quick no-code demosVery high volume (credits scale fast)High-volume, complex data transforms
↺ My honest, unsentimental take

I use all three. Zapier for quick client integrations when the app isn't in n8n's catalog and volume is low. Make for marketing teams who need visual clarity and moderate volume. n8n for everything I run myself at scale. The $847 Zapier bill didn't make me hate Zapier — it made me understand that per-execution pricing is a bad fit for scalable systems. Use the right tool for the volume and complexity.

M25
Expression reference — the complete cheatsheet
Every expression pattern you'll actually use, in one place

Data access

// Current item
{{ $json.fieldName }}                      // simple field
{{ $json["field-with-dashes"] }}           // bracket notation
{{ $json.address?.city }}                  // optional chaining (safe for null)
{{ $json.items[0].name }}                  // array index
{{ $json.contact?.email || 'no email' }}   // null coalescing

// Upstream nodes
{{ $node["Webhook"].json.body.email }}
{{ $node["GHL Create"].json.id }}

// Execution context
{{ $execution.id }}           // unique run ID
{{ $execution.mode }}         // 'manual' or 'trigger'
{{ $workflow.name }}          // workflow name
{{ $itemIndex }}              // 0-based index of current item

Date & time (Luxon)

{{ $now.toISO() }}                          // "2026-05-14T09:30:00.000Z"
{{ $now.toFormat('dd MMM yyyy') }}          // "14 May 2026"
{{ $now.toFormat('yyyy-MM-dd') }}           // "2026-05-14"
{{ $now.plus({days: 7}).toISO() }}          // 7 days from now
{{ $now.minus({months: 1}).toISO() }}       // 1 month ago
{{ $now.startOf('week').toISO() }}          // start of current week
{{ $now.endOf('month').toISO() }}           // end of current month
{{ $today }}                               // today at midnight UTC

String operations

{{ $json.name.toUpperCase() }}
{{ $json.name.trim() }}
{{ $json.name.replace(/\s+/g, '-') }}          // regex replace
{{ $json.email.split('@')[0] }}                 // split + index
{{ $json.description.substring(0, 100) + '...' }}// truncate
{{ [$json.firstName, $json.lastName].filter(Boolean).join(' ') }}

Numbers, arrays, conditionals

// Numbers
{{ Math.round($json.budget * 12) }}
{{ ($json.converted / $json.total * 100).toFixed(1) }}
{{ parseInt($json.budgetStr.replace(/[^0-9]/g,'')) }}

// Arrays
{{ $json.tags.length }}
{{ $json.tags.includes('vip') }}
{{ $json.tags.join(', ') }}
{{ $json.items.map(i => i.name).join(', ') }}
{{ $json.items.filter(i => i.active).length }}
{{ $json.scores.reduce((sum, s) => sum + s, 0) }}

// Conditionals
{{ $json.score >= 80 ? 'HOT' : $json.score >= 50 ? 'WARM' : 'COLD' }}
{{ $json.company || 'Individual' }}
{{ $execution.mode === 'manual' ? 'test@example.com' : $json.email }}

Code node patterns

// Deduplicate items by field
const seen = new Set();
return $input.all().filter(item => {
  const key = item.json.email;
  if (seen.has(key)) return false;
  seen.add(key);
  return true;
});

// Array → individual items
const rows = $input.first().json.contacts;
return rows.map(row => ({ json: row }));

// Aggregate multiple items into one
const items = $input.all();
return [{ json: {
  total: items.length,
  emails: items.map(i => i.json.email),
  sum: items.reduce((s, i) => s + i.json.budget, 0)
} }];

// Safe JSON parse (handle LLM output formatting)
function safeJson(str, fallback = {}) {
  try { return JSON.parse(str.replace(/```json|```/g, '').trim()); }
  catch { return fallback; }
}
const parsed = safeJson($json.llm_output);
SOURCES
Cited sources & reference documentation
Everything I drew from, in one place
01
n8n Official Documentation — docs.n8n.io
02
CVE-2026-21858 — FortiGuard Labs Advisory
03
n8n Security Hardening Guide — docs.n8n.io/hosting/securing
04
n8n Expressions Reference — docs.n8n.io/code/expressions
05
Luxon Date Library — moment.github.io/luxon
06
n8n AI Agent Documentation — docs.n8n.io/advanced-ai
07
Meta WhatsApp Cloud API — developers.facebook.com
08
GoHighLevel API v2 Reference — highlevel.stoplight.io
09
n8n Queue Mode — docs.n8n.io/hosting/scaling
10
Slack Block Kit Builder — api.slack.com/block-kit
11
n8n Sustainable Use License — n8n.io/legal
12
Make vs n8n Comparison — make.com/en/compare
Related Grimoires
005
Zapier 101: Business Automation
The Zapier alternative — managed infrastructure, no self-hosting. Different cost curve, same automation fundamentals.
006
Automations 101: The Absolute Guide
The pattern library behind everything you build in n8n. Start here when your workflows need architecture decisions, not just more nodes.
007
Agentic AI 101: Practical AI Workflows
Where n8n agents grow up. G004 teaches the AI Agent node. G007 teaches what to build around it.