1. Installation & Setup
Quick Start with Docker (Recommended)
# Run n8n with persistent data
docker run -it --rm --name n8n \
-p 5678:5678 \
-v n8n_data:/home/node/.n8n \
docker.n8n.io/n8nio/n8n
# Access UI at http://localhost:5678Docker Compose with PostgreSQL (Production)
# docker-compose.yml
version: '3.8'
services:
n8n:
image: docker.n8n.io/n8nio/n8n
restart: always
ports:
- "5678:5678"
environment:
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=n8n
- DB_POSTGRESDB_USER=n8n
- DB_POSTGRESDB_PASSWORD=${N8N_DB_PASSWORD}
- N8N_BASIC_AUTH_ACTIVE=true
- N8N_BASIC_AUTH_USER=admin
- N8N_BASIC_AUTH_PASSWORD=${N8N_AUTH_PASSWORD}
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- N8N_HOST=localhost
- N8N_PORT=5678
- N8N_PROTOCOL=http
- WEBHOOK_URL=http://localhost:5678/
- GENERIC_TIMEZONE=Europe/Zurich
volumes:
- n8n_data:/home/node/.n8n
- /home/markus/watched:/files/watched # local folder access
depends_on:
- postgres
postgres:
image: postgres:16-alpine
restart: always
environment:
- POSTGRES_USER=n8n
- POSTGRES_PASSWORD=${N8N_DB_PASSWORD}
- POSTGRES_DB=n8n
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
n8n_data:
postgres_data:NPM Global Install
npm install -g n8n
n8n start # starts on port 5678Key Environment Variables
| Variable | Purpose | Example |
|---|---|---|
| N8N_BASIC_AUTH_USER | UI login username | admin |
| N8N_BASIC_AUTH_PASSWORD | UI login password | supersecret |
| N8N_ENCRYPTION_KEY | Encrypt credentials at rest | random-32-char-string |
| WEBHOOK_URL | External webhook base URL | https://n8n.example.com/ |
| N8N_HOST | Bind host | 0.0.0.0 |
| N8N_PORT | Listen port | 5678 |
| N8N_PROTOCOL | http or https | https |
| GENERIC_TIMEZONE | Timezone for cron triggers | Europe/Zurich |
| EXECUTIONS_DATA_PRUNE | Auto-delete old executions | true |
| EXECUTIONS_DATA_MAX_AGE | Max age in hours | 168 |
2. Core Concepts
Building Blocks
- Nodes — individual steps (actions, triggers, logic)
- Triggers — start a workflow (cron, webhook, event)
- Connections — wires between nodes (data flows left→right)
- Workflows — a connected graph of nodes
- Executions — a single run of a workflow
- Credentials — stored auth (encrypted with N8N_ENCRYPTION_KEY)
- Pinned Data — mock input for testing nodes
- Sub-workflows — call one workflow from another
Key Expressions & Variables
// Access current item data
{{ $json.fieldName }}
{{ $json["nested"]["field"] }}
// Previous node output
{{ $node["NodeName"].json.field }}
// Input helpers
{{ $input.first().json.id }}
{{ $input.all().length }}
// Environment variables
{{ $env.MY_SECRET }}
// Execution metadata
{{ $execution.id }}
{{ $execution.mode }}
// Current item index
{{ $itemIndex }}
// Built-in methods
{{ $now.toISO() }}
{{ $today.toISO() }}
{{ DateTime.now().toFormat('yyyy-MM-dd') }}Error Handling
Error Trigger Workflow
Create a separate workflow with "Error Trigger" node. Set it as the error workflow in Settings → Error Workflow. Gets called on any unhandled failure.
Try/Catch Pattern
On any node: Settings → "Continue On Fail" = true. The error output goes to a separate branch. Check $json.error in the next node.
3. Trigger Nodes
| Trigger | Use Case | Key Config |
|---|---|---|
| Schedule Trigger | Run every X minutes/hours/days | Cron expression or interval (e.g. every 5 min) |
| Webhook | Receive HTTP POST/GET calls | Path, method, auth (Basic/Header) |
| Email Trigger (IMAP) | New email arrives | IMAP host, folder, poll interval |
| Local File Trigger | Watch folder for changes | Folder path, events (created/changed/deleted) |
| RSS Feed Trigger | New RSS feed items | Feed URL, poll interval |
| Manual Trigger | Click "Execute" in UI | Testing & development |
| n8n Trigger | Called by another workflow | Sub-workflow entry point |
| MQTT Trigger | MQTT message received | Broker URL, topic, QoS |
| Postgres Trigger | Poll DB for new/changed rows | Query, poll interval, tracking column |
4. Practical Example: File Organizer
Watch a folder → detect file type by extension → rename with date prefix → move to categorized subfolder (Documents/Images/Videos/Code).
Node Flow
Full Workflow JSON
{
"name": "File Organizer",
"nodes": [
{
"parameters": {
"path": "/files/watched/inbox",
"events": ["add"],
"options": {}
},
"name": "Watch Folder",
"type": "n8n-nodes-base.localFileTrigger",
"typeVersion": 1,
"position": [240, 300]
},
{
"parameters": {
"jsCode": "const path = require('path');\nconst items = $input.all();\nconst results = [];\nfor (const item of items) {\n const filePath = item.json.path;\n const ext = path.extname(filePath).toLowerCase();\n const baseName = path.basename(filePath);\n const datePrefix = new Date().toISOString().slice(0,10);\n const extMap = {\n '.pdf': 'Documents', '.doc': 'Documents', '.docx': 'Documents', '.txt': 'Documents', '.xlsx': 'Documents',\n '.jpg': 'Images', '.jpeg': 'Images', '.png': 'Images', '.gif': 'Images', '.webp': 'Images', '.svg': 'Images',\n '.mp4': 'Videos', '.mkv': 'Videos', '.avi': 'Videos', '.mov': 'Videos',\n '.js': 'Code', '.ts': 'Code', '.py': 'Code', '.sh': 'Code', '.go': 'Code', '.rs': 'Code', '.java': 'Code'\n };\n const category = extMap[ext] || 'Other';\n const newName = datePrefix + '_' + baseName;\n results.push({ json: { filePath, category, newName, baseName, ext } });\n}\nreturn results;"
},
"name": "Classify File",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [460, 300]
},
{
"parameters": {
"command": "=mkdir -p \"/files/watched/{{ $json.category }}\" && mv \"{{ $json.filePath }}\" \"/files/watched/{{ $json.category }}/{{ $json.newName }}\""
},
"name": "Move & Rename",
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [680, 300]
}
],
"connections": {
"Watch Folder": { "main": [[{ "node": "Classify File", "type": "main", "index": 0 }]] },
"Classify File": { "main": [[{ "node": "Move & Rename", "type": "main", "index": 0 }]] }
}
}Tip: Mount your local inbox folder into the Docker container via -v /home/user/Downloads:/files/watched/inbox. New downloads get auto-sorted!
5. Practical Example: Email Digest
Daily at 8am → Fetch unread emails (IMAP) → Filter important senders → Summarize with Ollama (local AI) → Send digest to Telegram.
Node Flow
Full Workflow JSON
{
"name": "Daily Email Digest",
"nodes": [
{
"parameters": {
"rule": { "interval": [{ "triggerAtHour": 8 }] }
},
"name": "Every Morning 8am",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.1,
"position": [240, 300]
},
{
"parameters": {
"mailbox": "INBOX",
"postProcessAction": "nothing",
"options": { "unseen": true }
},
"name": "Fetch Unread Emails",
"type": "n8n-nodes-base.imapEmail",
"typeVersion": 2,
"position": [440, 300],
"credentials": { "imap": { "id": "1", "name": "My IMAP" } }
},
{
"parameters": {
"conditions": {
"options": { "caseSensitive": false },
"combinator": "or",
"conditions": [
{ "leftValue": "={{ $json.from }}", "rightValue": "@company.com", "operator": { "type": "string", "operation": "contains" } },
{ "leftValue": "={{ $json.subject }}", "rightValue": "urgent", "operator": { "type": "string", "operation": "contains" } }
]
}
},
"name": "Filter Important",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [640, 300]
},
{
"parameters": {
"jsCode": "const emails = $input.all();\nlet summary = 'Email Digest:\\n\\n';\nfor (const e of emails) {\n summary += '• From: ' + e.json.from + '\\n';\n summary += ' Subject: ' + e.json.subject + '\\n\\n';\n}\nreturn [{ json: { prompt: 'Summarize these emails concisely:\\n' + summary, emails: emails.length } }];"
},
"name": "Format for AI",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [840, 300]
},
{
"parameters": {
"url": "http://host.docker.internal:11434/api/generate",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={ \"model\": \"llama3.1:8b\", \"prompt\": \"{{ $json.prompt }}\", \"stream\": false }"
},
"name": "Ollama Summarize",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [1040, 300]
},
{
"parameters": {
"chatId": "YOUR_CHAT_ID",
"text": "=📧 *Daily Email Digest*\n\n{{ $json.response }}"
},
"name": "Send to Telegram",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.1,
"position": [1240, 300],
"credentials": { "telegramApi": { "id": "2", "name": "My Telegram Bot" } }
}
],
"connections": {
"Every Morning 8am": { "main": [[{ "node": "Fetch Unread Emails", "type": "main", "index": 0 }]] },
"Fetch Unread Emails": { "main": [[{ "node": "Filter Important", "type": "main", "index": 0 }]] },
"Filter Important": { "main": [[{ "node": "Format for AI", "type": "main", "index": 0 }]] },
"Format for AI": { "main": [[{ "node": "Ollama Summarize", "type": "main", "index": 0 }]] },
"Ollama Summarize": { "main": [[{ "node": "Send to Telegram", "type": "main", "index": 0 }]] }
}
}Note: Use host.docker.internal to reach Ollama from inside Docker. On Linux, add --add-host=host.docker.internal:host-gateway to your Docker run command.
6. Practical Example: Git Backup Automation
Nightly at 2am → check git status → if changes exist → auto-commit & push → notify via Telegram. Perfect for auto-backing up dotfiles, notes, or config repos.
Node Flow
Full Workflow JSON
{
"name": "Git Auto-Backup",
"nodes": [
{
"parameters": {
"rule": { "interval": [{ "triggerAtHour": 2 }] }
},
"name": "Nightly 2am",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.1,
"position": [240, 300]
},
{
"parameters": {
"command": "cd /files/my-repo && git status --porcelain"
},
"name": "Check Git Status",
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [440, 300]
},
{
"parameters": {
"conditions": {
"conditions": [
{ "leftValue": "={{ $json.stdout }}", "rightValue": "", "operator": { "type": "string", "operation": "notEmpty" } }
]
}
},
"name": "Has Changes?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [640, 300]
},
{
"parameters": {
"command": "cd /files/my-repo && git add -A && git commit -m \"auto-backup: $(date +%Y-%m-%d_%H:%M)\" && git push origin main 2>&1"
},
"name": "Commit & Push",
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [840, 250]
},
{
"parameters": {
"chatId": "YOUR_CHAT_ID",
"text": "=✅ *Git Auto-Backup*\nRepo pushed successfully at {{ $now.toFormat('HH:mm') }}\n\n`{{ $json.stdout }}`"
},
"name": "Notify Success",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.1,
"position": [1040, 250],
"credentials": { "telegramApi": { "id": "2", "name": "My Telegram Bot" } }
},
{
"parameters": {
"chatId": "YOUR_CHAT_ID",
"text": "i️ *Git Auto-Backup*\nNo changes detected tonight."
},
"name": "No Changes",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.1,
"position": [840, 400],
"credentials": { "telegramApi": { "id": "2", "name": "My Telegram Bot" } }
}
],
"connections": {
"Nightly 2am": { "main": [[{ "node": "Check Git Status", "type": "main", "index": 0 }]] },
"Check Git Status": { "main": [[{ "node": "Has Changes?", "type": "main", "index": 0 }]] },
"Has Changes?": {
"main": [
[{ "node": "Commit & Push", "type": "main", "index": 0 }],
[{ "node": "No Changes", "type": "main", "index": 0 }]
]
},
"Commit & Push": { "main": [[{ "node": "Notify Success", "type": "main", "index": 0 }]] }
}
}7. Practical Example: System Monitor
Every 5 minutes → check disk usage, RAM, GPU temp → parse output → alert if disk >80% or GPU temp >80°C. Ideal for a local dev machine with NVIDIA GPUs.
Node Flow
Full Workflow JSON
{
"name": "System Monitor",
"nodes": [
{
"parameters": {
"rule": { "interval": [{ "field": "minutes", "minutesInterval": 5 }] }
},
"name": "Every 5 Minutes",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.1,
"position": [240, 300]
},
{
"parameters": {
"command": "echo '---DISK---' && df -h / | tail -1 && echo '---MEM---' && free -m | grep Mem && echo '---GPU---' && nvidia-smi --query-gpu=name,temperature.gpu,utilization.gpu,memory.used,memory.total --format=csv,noheader 2>/dev/null || echo 'NO_GPU'"
},
"name": "Get System Stats",
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [460, 300]
},
{
"parameters": {
"jsCode": "const output = $input.first().json.stdout;\nconst lines = output.split('\\n');\n\n// Parse disk\nconst diskLine = lines[1] || '';\nconst diskParts = diskLine.trim().split(/\\s+/);\nconst diskUsedPct = parseInt((diskParts[4] || '0').replace('%', ''));\n\n// Parse memory\nconst memLine = lines.find(l => l.startsWith('Mem:')) || '';\nconst memParts = memLine.trim().split(/\\s+/);\nconst memTotal = parseInt(memParts[1] || '0');\nconst memUsed = parseInt(memParts[2] || '0');\nconst memPct = memTotal > 0 ? Math.round((memUsed / memTotal) * 100) : 0;\n\n// Parse GPU\nconst gpuLine = lines.find(l => l.includes(',') && !l.includes('---')) || '';\nlet gpuTemp = 0, gpuName = 'N/A', gpuUtil = 'N/A', gpuMem = 'N/A';\nif (gpuLine && gpuLine !== 'NO_GPU') {\n const gp = gpuLine.split(',').map(s => s.trim());\n gpuName = gp[0]; gpuTemp = parseInt(gp[1]); gpuUtil = gp[2]; gpuMem = gp[3] + '/' + gp[4];\n}\n\nconst alert = diskUsedPct > 80 || gpuTemp > 80 || memPct > 90;\nreturn [{ json: { diskUsedPct, memPct, memUsed, memTotal, gpuName, gpuTemp, gpuUtil, gpuMem, alert } }];"
},
"name": "Parse Stats",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [680, 300]
},
{
"parameters": {
"conditions": {
"conditions": [
{ "leftValue": "={{ $json.alert }}", "rightValue": true, "operator": { "type": "boolean", "operation": "true" } }
]
}
},
"name": "Threshold Exceeded?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [900, 300]
},
{
"parameters": {
"chatId": "YOUR_CHAT_ID",
"text": "=🚨 *System Alert*\n\n💾 Disk: {{ $json.diskUsedPct }}%\n🧠 RAM: {{ $json.memUsed }}MB / {{ $json.memTotal }}MB ({{ $json.memPct }}%)\n🎮 GPU: {{ $json.gpuName }}\n🌡️ GPU Temp: {{ $json.gpuTemp }}°C\n📊 GPU Util: {{ $json.gpuUtil }}\n💽 GPU Mem: {{ $json.gpuMem }}"
},
"name": "Send Alert",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.1,
"position": [1120, 250],
"credentials": { "telegramApi": { "id": "2", "name": "My Telegram Bot" } }
}
],
"connections": {
"Every 5 Minutes": { "main": [[{ "node": "Get System Stats", "type": "main", "index": 0 }]] },
"Get System Stats": { "main": [[{ "node": "Parse Stats", "type": "main", "index": 0 }]] },
"Parse Stats": { "main": [[{ "node": "Threshold Exceeded?", "type": "main", "index": 0 }]] },
"Threshold Exceeded?": { "main": [[{ "node": "Send Alert", "type": "main", "index": 0 }]] }
}
}Important: For nvidia-smi to work inside Docker, add --gpus all or use the NVIDIA container runtime. For host-level monitoring, mount /usr/bin/nvidia-smi and the NVIDIA libs.
8. Practical Example: Web Scraper + Database
Daily → fetch a webpage → extract data with HTML Extract (Cheerio) → upsert to Postgres → notify on new entries. Practical for price monitoring, job listings, etc.
Node Flow
Full Workflow JSON
{
"name": "Web Scraper + Postgres",
"nodes": [
{
"parameters": {
"rule": { "interval": [{ "triggerAtHour": 6 }] }
},
"name": "Daily 6am",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.1,
"position": [240, 300]
},
{
"parameters": {
"url": "https://example.com/listings",
"options": { "response": { "response": { "fullResponse": true } } }
},
"name": "Fetch Page",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [440, 300]
},
{
"parameters": {
"operation": "extractHtmlContent",
"extractionValues": {
"values": [
{ "key": "title", "cssSelector": ".listing-title", "returnValue": "text" },
{ "key": "price", "cssSelector": ".listing-price", "returnValue": "text" },
{ "key": "link", "cssSelector": ".listing-title a", "returnValue": "attribute", "attribute": "href" }
]
}
},
"name": "Extract Data",
"type": "n8n-nodes-base.html",
"typeVersion": 1.2,
"position": [640, 300]
},
{
"parameters": {
"jsCode": "const titles = $input.first().json.title;\nconst prices = $input.first().json.price;\nconst links = $input.first().json.link;\nconst items = [];\nfor (let i = 0; i < titles.length; i++) {\n items.push({\n json: {\n title: titles[i],\n price: prices[i],\n link: links[i],\n scraped_at: new Date().toISOString()\n }\n });\n}\nreturn items;"
},
"name": "Transform",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [840, 300]
},
{
"parameters": {
"operation": "upsert",
"table": "scraped_listings",
"columns": "title, price, link, scraped_at",
"conflictColumns": "link"
},
"name": "Upsert to Postgres",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.3,
"position": [1040, 300],
"credentials": { "postgres": { "id": "3", "name": "Local Postgres" } }
},
{
"parameters": {
"chatId": "YOUR_CHAT_ID",
"text": "=🔍 *Scraper Update*\nProcessed {{ $input.all().length }} listings"
},
"name": "Notify",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.1,
"position": [1240, 300],
"credentials": { "telegramApi": { "id": "2", "name": "My Telegram Bot" } }
}
],
"connections": {
"Daily 6am": { "main": [[{ "node": "Fetch Page", "type": "main", "index": 0 }]] },
"Fetch Page": { "main": [[{ "node": "Extract Data", "type": "main", "index": 0 }]] },
"Extract Data": { "main": [[{ "node": "Transform", "type": "main", "index": 0 }]] },
"Transform": { "main": [[{ "node": "Upsert to Postgres", "type": "main", "index": 0 }]] },
"Upsert to Postgres": { "main": [[{ "node": "Notify", "type": "main", "index": 0 }]] }
}
}Postgres Table Setup
CREATE TABLE scraped_listings (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
price TEXT,
link TEXT UNIQUE NOT NULL,
scraped_at TIMESTAMPTZ DEFAULT NOW(),
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_listings_link ON scraped_listings(link);9. Practical Example: AI Document Processor
Watch folder for PDFs → extract text with pdftotext → summarize with Ollama (local LLM, no cloud APIs!) → save summary to file + log to Postgres.
Node Flow
Full Workflow JSON
{
"name": "AI Document Processor",
"nodes": [
{
"parameters": {
"path": "/files/watched/pdfs",
"events": ["add"],
"options": { "ignored": "**/*.!(pdf)" }
},
"name": "Watch PDFs",
"type": "n8n-nodes-base.localFileTrigger",
"typeVersion": 1,
"position": [240, 300]
},
{
"parameters": {
"command": "=pdftotext \"{{ $json.path }}\" - | head -5000"
},
"name": "Extract Text",
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [460, 300]
},
{
"parameters": {
"url": "http://host.docker.internal:11434/api/generate",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={ \"model\": \"llama3.1:8b\", \"prompt\": \"Summarize this document concisely in bullet points:\\n\\n{{ $json.stdout.substring(0, 4000) }}\", \"stream\": false }",
"options": { "timeout": 120000 }
},
"name": "Ollama Summarize",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [680, 300]
},
{
"parameters": {
"command": "=echo '{{ $json.response.replace(/'/g, "'\\''") }}' > \"/files/watched/summaries/$(basename '{{ $node[\"Watch PDFs\"].json.path }}' .pdf)_summary.txt\""
},
"name": "Save Summary",
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [900, 300]
},
{
"parameters": {
"operation": "insert",
"table": "document_summaries",
"columns": "filename, summary, processed_at",
"options": {}
},
"name": "Log to DB",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.3,
"position": [1120, 300],
"credentials": { "postgres": { "id": "3", "name": "Local Postgres" } }
},
{
"parameters": {
"chatId": "YOUR_CHAT_ID",
"text": "=📄 *Document Processed*\nFile: {{ $node[\"Watch PDFs\"].json.path }}\n\n{{ $node[\"Ollama Summarize\"].json.response.substring(0, 500) }}"
},
"name": "Notify",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.1,
"position": [1340, 300],
"credentials": { "telegramApi": { "id": "2", "name": "My Telegram Bot" } }
}
],
"connections": {
"Watch PDFs": { "main": [[{ "node": "Extract Text", "type": "main", "index": 0 }]] },
"Extract Text": { "main": [[{ "node": "Ollama Summarize", "type": "main", "index": 0 }]] },
"Ollama Summarize": { "main": [[{ "node": "Save Summary", "type": "main", "index": 0 }]] },
"Save Summary": { "main": [[{ "node": "Log to DB", "type": "main", "index": 0 }]] },
"Log to DB": { "main": [[{ "node": "Notify", "type": "main", "index": 0 }]] }
}
}Prerequisites: Install poppler-utils (for pdftotext) in your n8n Docker image or mount from host. Ollama must be running on the host with a model pulled (ollama pull llama3.1:8b).
10. Practical Example: Invoice Automation
Webhook receives order data → generate PDF invoice with Code node → send email → log to Postgres. Perfect for freelancers and small businesses.
Node Flow
Full Workflow JSON
{
"name": "Invoice Automation",
"nodes": [
{
"parameters": {
"path": "invoice",
"httpMethod": "POST",
"responseMode": "responseNode",
"options": {}
},
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1.1,
"position": [240, 300],
"webhookId": "invoice-gen"
},
{
"parameters": {
"jsCode": "const order = $input.first().json.body;\nconst invoiceNum = 'INV-' + Date.now();\nconst items = order.items || [];\nlet total = 0;\nlet rows = '';\nfor (const item of items) {\n const lineTotal = item.qty * item.price;\n total += lineTotal;\n rows += '<tr><td>' + item.name + '</td><td>' + item.qty + '</td><td>CHF ' + item.price.toFixed(2) + '</td><td>CHF ' + lineTotal.toFixed(2) + '</td></tr>';\n}\nconst html = '<!DOCTYPE html><html><head><style>body{font-family:Arial,sans-serif;margin:40px}table{width:100%;border-collapse:collapse;margin-top:20px}th,td{border:1px solid #ddd;padding:8px;text-align:left}th{background:#f5f5f5}.total{font-size:1.2em;font-weight:bold;margin-top:20px}</style></head><body><h1>Invoice ' + invoiceNum + '</h1><p>Date: ' + new Date().toISOString().slice(0,10) + '</p><p>To: ' + (order.customer_name || 'Customer') + '</p><p>' + (order.customer_email || '') + '</p><table><tr><th>Item</th><th>Qty</th><th>Price</th><th>Total</th></tr>' + rows + '</table><p class=\"total\">Total: CHF ' + total.toFixed(2) + '</p><hr><p><small>Veloz Engineering — velozengineering.ch</small></p></body></html>';\nreturn [{ json: { invoiceNum, html, total, customer_name: order.customer_name, customer_email: order.customer_email, items } }];"
},
"name": "Generate Invoice HTML",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [460, 300]
},
{
"parameters": {
"sendTo": "={{ $json.customer_email }}",
"subject": "=Invoice {{ $json.invoiceNum }}",
"emailType": "html",
"html": "={{ $json.html }}",
"options": {}
},
"name": "Send Email",
"type": "n8n-nodes-base.emailSend",
"typeVersion": 2.1,
"position": [680, 300],
"credentials": { "smtp": { "id": "4", "name": "SMTP" } }
},
{
"parameters": {
"operation": "insert",
"table": "invoices",
"columns": "invoice_number, customer_name, customer_email, total, created_at"
},
"name": "Log to DB",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.3,
"position": [900, 300],
"credentials": { "postgres": { "id": "3", "name": "Local Postgres" } }
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={ \"success\": true, \"invoice\": \"{{ $node[\"Generate Invoice HTML\"].json.invoiceNum }}\" }"
},
"name": "Respond",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [1120, 300]
}
],
"connections": {
"Webhook": { "main": [[{ "node": "Generate Invoice HTML", "type": "main", "index": 0 }]] },
"Generate Invoice HTML": { "main": [[{ "node": "Send Email", "type": "main", "index": 0 }]] },
"Send Email": { "main": [[{ "node": "Log to DB", "type": "main", "index": 0 }]] },
"Log to DB": { "main": [[{ "node": "Respond", "type": "main", "index": 0 }]] }
}
}Test the Webhook
curl -X POST http://localhost:5678/webhook/invoice \
-H "Content-Type: application/json" \
-d '{
"customer_name": "Max Mustermann",
"customer_email": "[email protected]",
"items": [
{"name": "Consulting (1h)", "qty": 4, "price": 150},
{"name": "Server Setup", "qty": 1, "price": 500}
]
}'11. Local AI Integration
n8n + Ollama = fully local AI workflows. No API keys, no cloud costs, full privacy. Your NVIDIA GPUs do the heavy lifting.
Ollama API via HTTP Request Node
Generate (completion)
// HTTP Request node config:
URL: http://host.docker.internal:11434/api/generate
Method: POST
Body (JSON):
{
"model": "llama3.1:8b",
"prompt": "{{ $json.text }}",
"stream": false,
"options": {
"temperature": 0.7,
"num_predict": 500
}
}
// Response: $json.responseChat (multi-turn)
// HTTP Request node config:
URL: http://host.docker.internal:11434/api/chat
Method: POST
Body (JSON):
{
"model": "llama3.1:8b",
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "{{ $json.question }}"}
],
"stream": false
}
// Response: $json.message.contentEmbeddings for RAG
// HTTP Request to Ollama embeddings endpoint
URL: http://host.docker.internal:11434/api/embeddings
Method: POST
Body:
{
"model": "nomic-embed-text",
"prompt": "{{ $json.text }}"
}
// Response: $json.embedding (array of floats)Local RAG Pipeline: n8n + Ollama + Qdrant
Ingest Pipeline:
[File Trigger] → [Extract Text] → [Code: Chunk text] → [HTTP: Ollama embed] → [HTTP: Qdrant upsert]
Query Pipeline:
[Webhook: question] → [HTTP: Ollama embed query] → [HTTP: Qdrant search] → [Code: build context] → [HTTP: Ollama generate with context] → [Respond]
Qdrant Docker Setup (for vector storage)
# Add to docker-compose.yml
qdrant:
image: qdrant/qdrant:latest
ports:
- "6333:6333"
volumes:
- qdrant_data:/qdrant/storage
# Create collection
curl -X PUT http://localhost:6333/collections/documents \
-H "Content-Type: application/json" \
-d '{"vectors": {"size": 768, "distance": "Cosine"}}'
# Upsert vectors (from n8n HTTP Request)
curl -X PUT http://localhost:6333/collections/documents/points \
-H "Content-Type: application/json" \
-d '{"points": [{"id": 1, "vector": [...], "payload": {"text": "chunk content"}}]}'
# Search (from n8n HTTP Request)
curl -X POST http://localhost:6333/collections/documents/points/search \
-H "Content-Type: application/json" \
-d '{"vector": [...], "limit": 5}'n8n Built-in AI Nodes (v1.19+)
- AI Agent — autonomous agent with tools (search, code, etc.)
- LangChain nodes — chain, memory, tools, output parsers
- Vector Store nodes — Qdrant, Pinecone, Supabase built-in
- Text Splitter — chunk documents for embedding
- Embeddings node — connects to Ollama natively
- Set Ollama as your LLM provider in n8n AI settings (no API key needed!)
12. Microsoft 365 Setup (Teams + Outlook)
Azure AD App Registration
n8n connects to Teams & Outlook via Microsoft Graph API. You need an Azure AD App:
- Go to portal.azure.com → Azure Active Directory → App Registrations → New
- Name: "n8n Automation", Redirect URI:
http://localhost:5678/rest/oauth2-credential/callback - Note the Application (client) ID and Directory (tenant) ID
- Certificates & Secrets → New client secret → copy the Value
- API Permissions → Add:
Mail.ReadWrite,Mail.Send,Calendars.ReadWrite,ChannelMessage.Send,Chat.ReadWrite,User.Read - Grant admin consent (or request from IT admin)
n8n Credential Setup
In n8n → Settings → Credentials → Add "Microsoft OAuth2":
# n8n Microsoft credential config
Client ID: <your-app-client-id>
Client Secret: <your-app-secret>
Tenant ID: <your-tenant-id> # or "common" for multi-tenant
Scope: https://graph.microsoft.com/.default
# For personal/work accounts, use:
Auth URL: https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/authorize
Token URL: https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/tokenMicrosoft Graph API Quick Reference
| Action | Endpoint | Permission |
|---|---|---|
| List emails | GET /me/messages | Mail.Read |
| Send email | POST /me/sendMail | Mail.Send |
| List calendar events | GET /me/events | Calendars.Read |
| Create event | POST /me/events | Calendars.ReadWrite |
| Send Teams message | POST /teams/{id}/channels/{id}/messages | ChannelMessage.Send |
| Send chat message | POST /chats/{id}/messages | Chat.ReadWrite |
| List Teams | GET /me/joinedTeams | Team.ReadBasic.All |
| Get presence | GET /me/presence | Presence.Read |
13. Example: Teams Automations
Daily Standup Reminder + Collector
Sends a standup prompt to a Teams channel every morning, collects replies, and posts a summary:
// Flow: Schedule (Mon-Fri 9:00) → Teams: Send Message → Wait (2h) →
// Teams: Get Replies → Code: Format Summary → Teams: Post Summary
// n8n Workflow JSON (import via CLI or paste in editor)
{
"nodes": [
{
"name": "Daily 9 AM",
"type": "n8n-nodes-base.scheduleTrigger",
"parameters": {
"rule": { "interval": [{ "field": "cronExpression", "expression": "0 9 * * 1-5" }] }
},
"position": [250, 300]
},
{
"name": "Post Standup Prompt",
"type": "n8n-nodes-base.microsoftTeams",
"parameters": {
"resource": "channelMessage",
"operation": "create",
"teamId": "={{ $env.TEAMS_TEAM_ID }}",
"channelId": "={{ $env.TEAMS_CHANNEL_ID }}",
"contentType": "html",
"message": "<b>🟢 Daily Standup</b><br>Please reply with:<br>1. What did you do yesterday?<br>2. What are you doing today?<br>3. Any blockers?"
},
"position": [450, 300]
}
]
}Deployment Notification to Teams
Webhook triggered by CI/CD pipeline → posts deployment status to Teams channel with Adaptive Card:
// Flow: Webhook (POST /deploy-notify) → Code: Build Card → Teams: Post
// Webhook receives:
// { "app": "my-service", "env": "production", "status": "success",
// "version": "1.2.3", "deployer": "markus", "url": "https://..." }
// Code Node — Build Adaptive Card:
const payload = $input.first().json;
const color = payload.status === 'success' ? '00FF00' : 'FF0000';
const emoji = payload.status === 'success' ? '✅' : '❌';
return [{
json: {
teamId: $env.TEAMS_TEAM_ID,
channelId: $env.TEAMS_CHANNEL_ID,
contentType: "html",
message: `<b>${emoji} Deployment ${payload.status.toUpperCase()}</b><br>
<b>App:</b> ${payload.app}<br>
<b>Version:</b> ${payload.version}<br>
<b>Environment:</b> ${payload.env}<br>
<b>Deployed by:</b> ${payload.deployer}<br>
<b>URL:</b> <a href="${payload.url}">${payload.url}</a>`
}
}];On-Call / Incident Alert
Webhook from monitoring (Prometheus/Grafana) → post urgent alert to Teams + tag on-call person:
// Flow: Webhook → Code: Format Alert → Teams: Post → IF critical →
// Teams: Send Chat to On-Call Person
// Code Node:
const alert = $input.first().json;
const severity = alert.status === 'firing' ? '🔴 CRITICAL' : '🟢 Resolved';
const onCall = {
'mon': '[email protected]', 'tue': '[email protected]',
'wed': '[email protected]', 'thu': '[email protected]',
'fri': '[email protected]'
};
const today = ['sun','mon','tue','wed','thu','fri','sat'][new Date().getDay()];
return [{
json: {
message: `<b>${severity}</b><br>
<b>Alert:</b> ${alert.labels?.alertname || 'Unknown'}<br>
<b>Service:</b> ${alert.labels?.service || 'N/A'}<br>
<b>Description:</b> ${alert.annotations?.description || ''}<br>
<b>On-Call:</b> ${onCall[today] || 'unassigned'}`,
onCallEmail: onCall[today],
isCritical: alert.status === 'firing'
}
}];Teams Presence → Auto Status
Check your Teams presence and auto-update a status page or mute notifications:
// Flow: Schedule (every 5min) → Microsoft Teams: Get Presence →
// IF "InAMeeting" → HTTP: Update Status Page / Mute Alerts
// HTTP Request Node — Check presence:
Method: GET
URL: https://graph.microsoft.com/v1.0/me/presence
Headers: Authorization = Bearer {{ $credentials.microsoftOAuth2.accessToken }}
// Response:
// { "availability": "Busy", "activity": "InAMeeting" }
// Possible: Available, Busy, DoNotDisturb, BeRightBack, Away, Offline
// Activities: InACall, InAMeeting, Presenting, Available, etc.
// IF Node condition:
// {{ $json.activity }} equals "InAMeeting" → mute route
// {{ $json.availability }} equals "DoNotDisturb" → suppress alertsJira/ServiceNow → Teams Bridge
When a Jira ticket is assigned to you, post it to your Teams channel:
// Flow: Webhook (Jira) → IF assignee=me → Code: Format → Teams: Post
// Jira Webhook payload extract (Code Node):
const issue = $input.first().json;
return [{
json: {
message: `<b>📋 New Ticket Assigned</b><br>
<b>${issue.key}</b>: ${issue.fields.summary}<br>
<b>Priority:</b> ${issue.fields.priority.name}<br>
<b>Type:</b> ${issue.fields.issuetype.name}<br>
<a href="https://jira.company.com/browse/${issue.key}">Open in Jira →</a>`
}
}];14. Example: Outlook Automations
Smart Email Categorizer with AI
Fetch unread emails → classify with Ollama → auto-move to folders → daily summary:
// Flow: Schedule (every 30min) → Outlook: Get Unread →
// Loop → Ollama: Classify → Outlook: Move to Folder →
// Aggregate → IF end of day → Teams: Post Summary
// Step 1: Fetch unread (HTTP Request or Microsoft Outlook node)
Method: GET
URL: https://graph.microsoft.com/v1.0/me/mailFolders/Inbox/messages
?$filter=isRead eq false&$top=50&$select=subject,from,bodyPreview,receivedDateTime
// Step 2: Ollama Classification (HTTP Request Node)
Method: POST
URL: http://localhost:11434/api/generate
Body:
{
"model": "llama3.1:8b",
"prompt": "Classify this email into exactly ONE category: ACTION_REQUIRED, FYI, MEETING, JIRA, APPROVAL, SPAM. Only reply with the category name.\n\nFrom: {{ $json.from.emailAddress.address }}\nSubject: {{ $json.subject }}\nPreview: {{ $json.bodyPreview }}",
"stream": false
}
// Step 3: Move to folder (HTTP Request Node)
Method: POST
URL: https://graph.microsoft.com/v1.0/me/messages/{{ $json.id }}/move
Body: { "destinationId": "{{ $json.folderId }}" }
// Folder mapping (Code Node):
const folderMap = {
'ACTION_REQUIRED': 'AAMkAG...actionFolder',
'FYI': 'AAMkAG...fyiFolder',
'MEETING': 'AAMkAG...meetingFolder',
'JIRA': 'AAMkAG...jiraFolder',
'APPROVAL': 'AAMkAG...approvalFolder',
'SPAM': 'AAMkAG...junkFolder'
};Calendar → Daily Agenda Email
Every morning: fetch today's calendar events → format nice agenda → send to yourself or Teams:
// Flow: Schedule (Mon-Fri 7:30) → Outlook: Get Events → Code: Format →
// Send Email / Teams Message
// Fetch today's events (HTTP Request):
Method: GET
URL: https://graph.microsoft.com/v1.0/me/calendarView
?startDateTime={{ $now.startOf('day').toISO() }}
&endDateTime={{ $now.endOf('day').toISO() }}
&$orderby=start/dateTime
&$select=subject,start,end,location,organizer,isOnlineMeeting,onlineMeeting
// Code Node — Format Agenda:
const events = $input.first().json.value || [];
if (events.length === 0) return [{ json: { agenda: 'No meetings today! 🎉', count: 0 }}];
let agenda = '<b>📅 Your Agenda — ' + new Date().toLocaleDateString('de-CH') + '</b><br><br>';
let totalMeetingHours = 0;
for (const evt of events) {
const start = new Date(evt.start.dateTime + 'Z');
const end = new Date(evt.end.dateTime + 'Z');
const duration = (end - start) / 3600000;
totalMeetingHours += duration;
const time = start.toLocaleTimeString('de-CH', {hour:'2-digit', minute:'2-digit'})
+ ' - ' + end.toLocaleTimeString('de-CH', {hour:'2-digit', minute:'2-digit'});
const meetLink = evt.isOnlineMeeting ?
' <a href="' + (evt.onlineMeeting?.joinUrl || '#') + '">Join →</a>' : '';
agenda += `<b>${time}</b> ${evt.subject}${meetLink}<br>`;
if (evt.location?.displayName) agenda += ` 📍 ${evt.location.displayName}<br>`;
}
agenda += `<br><b>Total meeting time: ${totalMeetingHours.toFixed(1)}h</b>`;
if (totalMeetingHours > 6) agenda += ' ⚠️ Heavy meeting day!';
return [{ json: { agenda, count: events.length, hours: totalMeetingHours }}];Auto-Reply When Busy
If you're in a meeting and receive an email marked urgent, auto-reply with ETA:
// Flow: Outlook Trigger (new email) → IF importance=high →
// Get Presence → IF InAMeeting → Get Current Event End Time →
// Outlook: Reply with ETA
// Code Node — Build auto-reply:
const email = $('Outlook Trigger').first().json;
const currentEvent = $('Get Calendar').first().json.value?.[0];
const endTime = currentEvent ?
new Date(currentEvent.end.dateTime + 'Z').toLocaleTimeString('de-CH',
{hour:'2-digit', minute:'2-digit'}) : 'soon';
return [{
json: {
to: email.from.emailAddress.address,
subject: 'Re: ' + email.subject,
body: `Hi,\n\nI'm currently in a meeting and will get back to you around ${endTime}.\n\nIf this is truly urgent, please reach me on Teams.\n\nBest regards,\nMarkus`,
inReplyTo: email.id
}
}];
// Send Reply (HTTP Request):
Method: POST
URL: https://graph.microsoft.com/v1.0/me/messages/{{ $json.inReplyTo }}/reply
Body: { "message": { "body": { "content": "{{ $json.body }}", "contentType": "text" }}}Meeting Notes Extractor
After a meeting ends → check for Teams meeting transcript → summarize with AI → email notes to attendees:
// Flow: Schedule (every 15min) → Get Recently Ended Meetings →
// Loop → Get Transcript → Ollama: Summarize →
// Send Notes Email to Attendees
// Fetch meetings that ended in last 15 min:
Method: GET
URL: https://graph.microsoft.com/v1.0/me/calendarView
?startDateTime={{ $now.minus({minutes: 20}).toISO() }}
&endDateTime={{ $now.minus({minutes: 1}).toISO() }}
&$filter=isOnlineMeeting eq true
// Ollama Summarize (HTTP Request):
Method: POST
URL: http://localhost:11434/api/generate
Body: {
"model": "llama3.1:8b",
"prompt": "Summarize these meeting notes. Include: Key Decisions, Action Items (with owners), Open Questions.\n\nTranscript:\n{{ $json.transcript }}",
"stream": false
}Attachment Auto-Saver
Auto-download attachments from specific senders to organized local folders:
// Flow: Schedule (every 10min) → Outlook: Get Emails with Attachments →
// Loop → Download Attachment → Categorize → Save to Folder
// Fetch emails with attachments:
Method: GET
URL: https://graph.microsoft.com/v1.0/me/messages
?$filter=hasAttachments eq true and isRead eq false
&$expand=attachments&$top=20
// Code Node — Save logic:
const msg = $input.first().json;
const senderRules = {
'invoice': /buchhaltung|accounting|rechnung|invoice/i,
'contracts': /vertrag|contract|legal/i,
'reports': /report|bericht|weekly|monthly/i,
};
for (const att of msg.attachments || []) {
if (att['@odata.type'] !== '#microsoft.graph.fileAttachment') continue;
let folder = 'general';
for (const [cat, regex] of Object.entries(senderRules)) {
if (regex.test(msg.subject) || regex.test(msg.from.emailAddress.address)) {
folder = cat; break;
}
}
return [{
json: {
filename: att.name,
content: att.contentBytes, // base64
folder: '/home/markus/Documents/outlook-attachments/' + folder,
sender: msg.from.emailAddress.address
}
}];
}
// Write File Node:
// Path: {{ $json.folder }}/{{ $json.filename }}
// Content: {{ $json.content }} (base64 decode)Weekly Time Report
Every Friday: analyze your week's calendar → generate time report → email to yourself:
// Flow: Schedule (Fri 17:00) → Get This Week's Events →
// Code: Analyze → Format Report → Email
// Code Node — Weekly Analysis:
const events = $input.first().json.value || [];
const categories = {};
let totalHours = 0;
for (const evt of events) {
const start = new Date(evt.start.dateTime + 'Z');
const end = new Date(evt.end.dateTime + 'Z');
const hours = (end - start) / 3600000;
totalHours += hours;
// Categorize by keywords in subject
let cat = 'Other';
if (/standup|daily|sync/i.test(evt.subject)) cat = 'Standups & Syncs';
else if (/review|code review|pr/i.test(evt.subject)) cat = 'Code Reviews';
else if (/planning|sprint|retro|grooming/i.test(evt.subject)) cat = 'Agile Ceremonies';
else if (/1:1|1-1|one.on.one/i.test(evt.subject)) cat = '1:1 Meetings';
else if (/workshop|training/i.test(evt.subject)) cat = 'Workshops';
else if (/interview/i.test(evt.subject)) cat = 'Interviews';
categories[cat] = (categories[cat] || 0) + hours;
}
const sorted = Object.entries(categories).sort((a,b) => b[1] - a[1]);
let report = '<b>📊 Weekly Meeting Report</b><br><br>';
report += '<b>Total: ' + totalHours.toFixed(1) + 'h in meetings</b><br>';
report += '<b>Focus time: ' + (40 - totalHours).toFixed(1) + 'h available</b><br><br>';
for (const [cat, hrs] of sorted) {
const bar = '█'.repeat(Math.round(hrs * 2)) + '░'.repeat(Math.round((10-hrs)*2));
report += `${cat}: ${hrs.toFixed(1)}h ${bar}<br>`;
}
if (totalHours > 25) report += '<br>⚠️ Over 60% in meetings — consider declining some!';
return [{ json: { report, totalHours }}];12. Advanced Patterns & Tips
CLI Commands
# Execute a workflow by ID
n8n execute --id 5
# Export all workflows
n8n export:workflow --all --output=./backups/
# Export single workflow
n8n export:workflow --id 5 --output=./workflow-5.json
# Import workflows
n8n import:workflow --input=./workflow-5.json
# Export credentials (encrypted)
n8n export:credentials --all --output=./creds/
# Import credentials
n8n import:credentials --input=./creds/creds.json
# List workflows
n8n list:workflowError Handling Patterns
Global Error Workflow
// Create a workflow with "Error Trigger" node
// → Receives: execution ID, workflow name, error message
// → Send alert to Telegram/Email/Slack
// Set in: Settings → Error Workflow → select itRetry on Fail
// On any node → Settings:
// "Retry On Fail" = true
// "Max Tries" = 3
// "Wait Between Tries (ms)" = 1000
//
// Great for flaky APIs and network callsWebhook Security
// Method 1: Basic Auth on webhook
// Webhook node → Authentication → Basic Auth
// Method 2: Header validation in Code node
const authHeader = $input.first().json.headers['x-api-key'];
if (authHeader !== $env.WEBHOOK_SECRET) {
throw new Error('Unauthorized');
}
// Method 3: IP allowlist in Code node
const clientIp = $input.first().json.headers['x-forwarded-for'];
const allowed = ['192.168.1.0/24', '10.0.0.0/8'];
// validate...Performance: Queue Mode with Redis
# docker-compose addition for scaling
redis:
image: redis:7-alpine
ports:
- "6379:6379"
n8n-worker:
image: docker.n8n.io/n8nio/n8n
command: worker
environment:
- EXECUTIONS_MODE=queue
- QUEUE_BULL_REDIS_HOST=redis
- QUEUE_BULL_REDIS_PORT=6379
# ... same DB config as main n8n
# Main n8n also needs:
# EXECUTIONS_MODE=queue
# QUEUE_BULL_REDIS_HOST=redisn8n REST API
# Enable API: set N8N_PUBLIC_API_DISABLED=false
# Generate API key in n8n UI: Settings → API
# List workflows
curl -H "X-N8N-API-KEY: your-key" http://localhost:5678/api/v1/workflows
# Execute a workflow
curl -X POST -H "X-N8N-API-KEY: your-key" \
http://localhost:5678/api/v1/workflows/5/activate
# Get executions
curl -H "X-N8N-API-KEY: your-key" \
http://localhost:5678/api/v1/executions?workflowId=5&limit=10Backup Strategy
#!/bin/bash
# backup-n8n.sh — run nightly via cron
BACKUP_DIR="/backups/n8n/$(date +%Y-%m-%d)"
mkdir -p "$BACKUP_DIR"
# Export all workflows
docker exec n8n n8n export:workflow --all --output=/tmp/workflows/
docker cp n8n:/tmp/workflows/ "$BACKUP_DIR/workflows/"
# Export credentials
docker exec n8n n8n export:credentials --all --output=/tmp/creds.json
docker cp n8n:/tmp/creds.json "$BACKUP_DIR/credentials.json"
# Postgres dump
docker exec postgres pg_dump -U n8n n8n > "$BACKUP_DIR/n8n-db.sql"
# Compress
tar -czf "$BACKUP_DIR.tar.gz" "$BACKUP_DIR"
rm -rf "$BACKUP_DIR"
echo "Backup complete: $BACKUP_DIR.tar.gz"Quick Tips
- ▸Use sub-workflows for reusable logic (error handling, notifications)
- ▸Store secrets in environment variables, never in workflow JSON
- ▸Use Pinned Data to mock inputs during development
- ▸Enable execution pruning to avoid DB bloat
- ▸Install community nodes via Settings → Community Nodes
- ▸Version control workflows: export JSON → commit to Git
- ▸Use tags to organize workflows (e.g. "production", "monitoring")
- ▸Set GENERIC_TIMEZONE so cron triggers fire at the right time