n8n Automation

n8n Notification Automation: Multi-Channel Alert System Design

DeviDevs Team
14 min read
#n8n#notifications#automation#alerting#slack#email#sms#webhooks

n8n Notification Automation: Multi-Channel Alert System Design

Effective notification systems are crucial for incident response, user engagement, and operational visibility. This guide covers building intelligent, multi-channel notification systems with n8n that route alerts appropriately and prevent notification fatigue.

Notification Architecture

Core Design Principles

A well-designed notification system should:

  1. Route intelligently - Send alerts to the right people through the right channels
  2. Prevent fatigue - Aggregate similar alerts and respect quiet hours
  3. Escalate appropriately - Ensure critical alerts reach someone
  4. Track delivery - Confirm notifications are received and acknowledged

Multi-Channel Router

// notification-router.js
/**
 * Intelligent notification routing logic for n8n.
 */
 
const CHANNEL_PRIORITY = {
  'critical': ['phone', 'sms', 'slack', 'email'],
  'high': ['slack', 'sms', 'email'],
  'medium': ['slack', 'email'],
  'low': ['email'],
  'info': ['email']
};
 
const USER_PREFERENCES = {
  // Would typically come from database
  'user_123': {
    preferred_channels: ['slack', 'email'],
    quiet_hours: { start: 22, end: 7 },
    timezone: 'America/New_York',
    phone: '+1234567890',
    slack_id: 'U12345',
    email: 'user@example.com'
  }
};
 
/**
 * Determine notification channels based on severity and user preferences
 */
function determineChannels(notification) {
  const { severity, user_id, bypass_preferences } = notification;
  const user = USER_PREFERENCES[user_id] || {};
 
  // Get channels for severity level
  let channels = CHANNEL_PRIORITY[severity] || CHANNEL_PRIORITY['low'];
 
  // Check quiet hours (unless critical)
  if (!bypass_preferences && severity !== 'critical') {
    if (isQuietHours(user)) {
      // During quiet hours, only use async channels
      channels = channels.filter(c => ['email'].includes(c));
    }
  }
 
  // Filter by user preferences (unless critical)
  if (!bypass_preferences && severity !== 'critical' && user.preferred_channels) {
    channels = channels.filter(c => user.preferred_channels.includes(c));
  }
 
  // Ensure at least one channel
  if (channels.length === 0) {
    channels = ['email'];
  }
 
  return channels;
}
 
/**
 * Check if current time is within user's quiet hours
 */
function isQuietHours(user) {
  if (!user.quiet_hours) return false;
 
  const now = new Date();
  // In production, convert to user's timezone
  const hour = now.getHours();
 
  const { start, end } = user.quiet_hours;
 
  if (start > end) {
    // Quiet hours span midnight
    return hour >= start || hour < end;
  } else {
    return hour >= start && hour < end;
  }
}
 
/**
 * Format notification for specific channel
 */
function formatForChannel(notification, channel) {
  const { title, message, severity, metadata, action_url } = notification;
 
  switch (channel) {
    case 'slack':
      return {
        blocks: [
          {
            type: 'header',
            text: {
              type: 'plain_text',
              text: `${getSeverityEmoji(severity)} ${title}`
            }
          },
          {
            type: 'section',
            text: {
              type: 'mrkdwn',
              text: message
            }
          },
          ...(metadata ? [{
            type: 'context',
            elements: Object.entries(metadata).map(([k, v]) => ({
              type: 'mrkdwn',
              text: `*${k}:* ${v}`
            }))
          }] : []),
          ...(action_url ? [{
            type: 'actions',
            elements: [{
              type: 'button',
              text: { type: 'plain_text', text: 'View Details' },
              url: action_url,
              style: severity === 'critical' ? 'danger' : 'primary'
            }]
          }] : [])
        ]
      };
 
    case 'email':
      return {
        subject: `[${severity.toUpperCase()}] ${title}`,
        html: `
          <div style="font-family: Arial, sans-serif; max-width: 600px;">
            <div style="background: ${getSeverityColor(severity)}; color: white; padding: 15px;">
              <h2 style="margin: 0;">${title}</h2>
            </div>
            <div style="padding: 20px; background: #f9f9f9;">
              <p>${message}</p>
              ${metadata ? `
                <table style="width: 100%; border-collapse: collapse;">
                  ${Object.entries(metadata).map(([k, v]) => `
                    <tr>
                      <td style="padding: 5px; font-weight: bold;">${k}</td>
                      <td style="padding: 5px;">${v}</td>
                    </tr>
                  `).join('')}
                </table>
              ` : ''}
              ${action_url ? `
                <p style="margin-top: 20px;">
                  <a href="${action_url}" style="background: #007bff; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">
                    View Details
                  </a>
                </p>
              ` : ''}
            </div>
          </div>
        `
      };
 
    case 'sms':
      return {
        body: `[${severity.toUpperCase()}] ${title}\n${message.substring(0, 140)}${action_url ? `\n${action_url}` : ''}`
      };
 
    case 'phone':
      return {
        twiml: `
          <Response>
            <Say voice="alice">
              ${severity} alert. ${title}. ${message}
            </Say>
            <Pause length="1"/>
            <Say voice="alice">
              Press 1 to acknowledge. Press 2 to escalate.
            </Say>
            <Gather numDigits="1" action="/handle-response"/>
          </Response>
        `
      };
 
    default:
      return { title, message, severity, metadata };
  }
}
 
function getSeverityEmoji(severity) {
  const emojis = {
    'critical': '🚨',
    'high': '🔴',
    'medium': '🟡',
    'low': '🟢',
    'info': 'ℹ️'
  };
  return emojis[severity] || '📢';
}
 
function getSeverityColor(severity) {
  const colors = {
    'critical': '#dc3545',
    'high': '#fd7e14',
    'medium': '#ffc107',
    'low': '#28a745',
    'info': '#17a2b8'
  };
  return colors[severity] || '#6c757d';
}
 
module.exports = {
  determineChannels,
  formatForChannel,
  isQuietHours,
  CHANNEL_PRIORITY
};

Alert Aggregation Workflow

{
  "name": "Alert Aggregation and Deduplication",
  "nodes": [
    {
      "name": "Webhook Trigger",
      "type": "n8n-nodes-base.webhook",
      "parameters": {
        "path": "alerts",
        "httpMethod": "POST"
      },
      "position": [250, 300]
    },
    {
      "name": "Normalize Alert",
      "type": "n8n-nodes-base.function",
      "parameters": {
        "functionCode": "// Normalize incoming alert to standard format\nconst alert = $input.first().json;\n\nconst normalized = {\n  id: alert.id || `alert_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,\n  source: alert.source || 'unknown',\n  severity: (alert.severity || alert.priority || 'medium').toLowerCase(),\n  title: alert.title || alert.name || alert.summary || 'Alert',\n  message: alert.message || alert.description || alert.body || '',\n  timestamp: alert.timestamp || new Date().toISOString(),\n  fingerprint: null,\n  metadata: {\n    ...alert.labels,\n    ...alert.tags,\n    source_id: alert.id\n  }\n};\n\n// Generate fingerprint for deduplication\n// Same fingerprint = same alert type\nconst fingerprintData = [\n  normalized.source,\n  normalized.title,\n  JSON.stringify(normalized.metadata)\n].join('|');\n\nnormalized.fingerprint = require('crypto')\n  .createHash('md5')\n  .update(fingerprintData)\n  .digest('hex');\n\nreturn { json: normalized };"
      },
      "position": [450, 300]
    },
    {
      "name": "Check Dedup Cache",
      "type": "n8n-nodes-base.redis",
      "parameters": {
        "operation": "get",
        "key": "={{ 'alert_dedup:' + $json.fingerprint }}"
      },
      "position": [650, 300]
    },
    {
      "name": "Is Duplicate?",
      "type": "n8n-nodes-base.if",
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.value !== null }}",
              "value2": true
            }
          ]
        }
      },
      "position": [850, 300]
    },
    {
      "name": "Increment Counter",
      "type": "n8n-nodes-base.function",
      "parameters": {
        "functionCode": "// Parse existing dedup record\nconst existing = JSON.parse($('Check Dedup Cache').item.json.value || '{}');\nconst alert = $('Normalize Alert').item.json;\n\nconst updated = {\n  fingerprint: alert.fingerprint,\n  first_seen: existing.first_seen || alert.timestamp,\n  last_seen: alert.timestamp,\n  count: (existing.count || 0) + 1,\n  severity: alert.severity,\n  title: alert.title,\n  suppressed: true\n};\n\n// Check if we should send aggregated notification\nconst shouldNotify = \n  updated.count === 5 || // First threshold\n  updated.count === 25 || // Second threshold\n  updated.count === 100 || // Third threshold\n  updated.count % 500 === 0; // Every 500 after\n\nreturn {\n  json: {\n    ...updated,\n    should_notify: shouldNotify,\n    original_alert: alert\n  }\n};"
      },
      "position": [1050, 200]
    },
    {
      "name": "Update Dedup Cache (Dup)",
      "type": "n8n-nodes-base.redis",
      "parameters": {
        "operation": "set",
        "key": "={{ 'alert_dedup:' + $json.fingerprint }}",
        "value": "={{ JSON.stringify($json) }}",
        "expire": true,
        "ttl": 3600
      },
      "position": [1250, 200]
    },
    {
      "name": "Should Send Aggregate?",
      "type": "n8n-nodes-base.if",
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.should_notify }}",
              "value2": true
            }
          ]
        }
      },
      "position": [1450, 200]
    },
    {
      "name": "Create Aggregate Alert",
      "type": "n8n-nodes-base.function",
      "parameters": {
        "functionCode": "const data = $input.first().json;\n\nreturn {\n  json: {\n    id: `agg_${data.fingerprint}_${Date.now()}`,\n    severity: data.severity,\n    title: `[AGGREGATED x${data.count}] ${data.title}`,\n    message: `This alert has occurred ${data.count} times since ${data.first_seen}. Last occurrence: ${data.last_seen}`,\n    metadata: {\n      aggregate_count: data.count,\n      first_occurrence: data.first_seen,\n      last_occurrence: data.last_seen,\n      fingerprint: data.fingerprint\n    },\n    timestamp: new Date().toISOString()\n  }\n};"
      },
      "position": [1650, 100]
    },
    {
      "name": "New Alert - Set Cache",
      "type": "n8n-nodes-base.redis",
      "parameters": {
        "operation": "set",
        "key": "={{ 'alert_dedup:' + $('Normalize Alert').item.json.fingerprint }}",
        "value": "={{ JSON.stringify({ fingerprint: $('Normalize Alert').item.json.fingerprint, first_seen: $('Normalize Alert').item.json.timestamp, last_seen: $('Normalize Alert').item.json.timestamp, count: 1, severity: $('Normalize Alert').item.json.severity, title: $('Normalize Alert').item.json.title }) }}",
        "expire": true,
        "ttl": 3600
      },
      "position": [1050, 400]
    },
    {
      "name": "Route to Channels",
      "type": "n8n-nodes-base.function",
      "parameters": {
        "functionCode": "const alert = $input.first().json;\n\n// Determine channels based on severity\nconst channelMap = {\n  'critical': ['slack', 'sms', 'email', 'pagerduty'],\n  'high': ['slack', 'email', 'pagerduty'],\n  'medium': ['slack', 'email'],\n  'low': ['slack'],\n  'info': ['email']\n};\n\nconst channels = channelMap[alert.severity] || channelMap['medium'];\n\nreturn channels.map(channel => ({\n  json: {\n    channel,\n    alert\n  }\n}));"
      },
      "position": [1850, 300]
    },
    {
      "name": "Channel Router",
      "type": "n8n-nodes-base.switch",
      "parameters": {
        "dataType": "string",
        "value1": "={{ $json.channel }}",
        "rules": {
          "rules": [
            { "value2": "slack", "output": 0 },
            { "value2": "email", "output": 1 },
            { "value2": "sms", "output": 2 },
            { "value2": "pagerduty", "output": 3 }
          ]
        }
      },
      "position": [2050, 300]
    },
    {
      "name": "Send Slack",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#alerts",
        "blocksUi": {
          "blocksValues": [
            {
              "type": "header",
              "text": {
                "type": "plain_text",
                "text": "={{ $json.alert.severity === 'critical' ? '🚨' : $json.alert.severity === 'high' ? '🔴' : '🟡' }} {{ $json.alert.title }}"
              }
            },
            {
              "type": "section",
              "text": {
                "type": "mrkdwn",
                "text": "={{ $json.alert.message }}"
              }
            },
            {
              "type": "context",
              "elements": [
                {
                  "type": "mrkdwn",
                  "text": "*Source:* {{ $json.alert.source || 'Unknown' }} | *Time:* {{ $json.alert.timestamp }}"
                }
              ]
            }
          ]
        }
      },
      "position": [2250, 100]
    },
    {
      "name": "Send Email",
      "type": "n8n-nodes-base.emailSend",
      "parameters": {
        "fromEmail": "alerts@example.com",
        "toEmail": "oncall@example.com",
        "subject": "=[{{ $json.alert.severity.toUpperCase() }}] {{ $json.alert.title }}",
        "html": "=<h2>{{ $json.alert.title }}</h2><p>{{ $json.alert.message }}</p><p><small>Source: {{ $json.alert.source }} | Time: {{ $json.alert.timestamp }}</small></p>"
      },
      "position": [2250, 250]
    },
    {
      "name": "Send SMS",
      "type": "n8n-nodes-base.twilio",
      "parameters": {
        "operation": "send",
        "from": "={{ $env.TWILIO_PHONE }}",
        "to": "={{ $env.ONCALL_PHONE }}",
        "message": "=[{{ $json.alert.severity.toUpperCase() }}] {{ $json.alert.title }}: {{ $json.alert.message.substring(0, 100) }}"
      },
      "position": [2250, 400]
    },
    {
      "name": "Create PagerDuty Incident",
      "type": "n8n-nodes-base.httpRequest",
      "parameters": {
        "method": "POST",
        "url": "https://events.pagerduty.com/v2/enqueue",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "jsonParameters": true,
        "options": {},
        "bodyParametersJson": "={\n  \"routing_key\": \"{{ $env.PAGERDUTY_ROUTING_KEY }}\",\n  \"event_action\": \"trigger\",\n  \"dedup_key\": \"{{ $json.alert.id }}\",\n  \"payload\": {\n    \"summary\": \"{{ $json.alert.title }}\",\n    \"severity\": \"{{ $json.alert.severity === 'critical' ? 'critical' : $json.alert.severity === 'high' ? 'error' : 'warning' }}\",\n    \"source\": \"{{ $json.alert.source }}\",\n    \"custom_details\": {{ JSON.stringify($json.alert.metadata || {}) }}\n  }\n}"
      },
      "position": [2250, 550]
    }
  ],
  "connections": {
    "Webhook Trigger": {
      "main": [[{"node": "Normalize Alert", "type": "main", "index": 0}]]
    },
    "Normalize Alert": {
      "main": [[{"node": "Check Dedup Cache", "type": "main", "index": 0}]]
    },
    "Check Dedup Cache": {
      "main": [[{"node": "Is Duplicate?", "type": "main", "index": 0}]]
    },
    "Is Duplicate?": {
      "main": [
        [{"node": "Increment Counter", "type": "main", "index": 0}],
        [{"node": "New Alert - Set Cache", "type": "main", "index": 0}]
      ]
    },
    "Increment Counter": {
      "main": [[{"node": "Update Dedup Cache (Dup)", "type": "main", "index": 0}]]
    },
    "Update Dedup Cache (Dup)": {
      "main": [[{"node": "Should Send Aggregate?", "type": "main", "index": 0}]]
    },
    "Should Send Aggregate?": {
      "main": [
        [{"node": "Create Aggregate Alert", "type": "main", "index": 0}],
        []
      ]
    },
    "Create Aggregate Alert": {
      "main": [[{"node": "Route to Channels", "type": "main", "index": 0}]]
    },
    "New Alert - Set Cache": {
      "main": [[{"node": "Route to Channels", "type": "main", "index": 0}]]
    },
    "Route to Channels": {
      "main": [[{"node": "Channel Router", "type": "main", "index": 0}]]
    },
    "Channel Router": {
      "main": [
        [{"node": "Send Slack", "type": "main", "index": 0}],
        [{"node": "Send Email", "type": "main", "index": 0}],
        [{"node": "Send SMS", "type": "main", "index": 0}],
        [{"node": "Create PagerDuty Incident", "type": "main", "index": 0}]
      ]
    }
  }
}

Escalation Policy Workflow

// escalation-policy.js
/**
 * Escalation policy implementation for n8n.
 */
 
const ESCALATION_POLICIES = {
  'default': {
    name: 'Default Escalation',
    levels: [
      {
        level: 1,
        delay_minutes: 0,
        targets: [{ type: 'schedule', id: 'primary_oncall' }],
        channels: ['slack', 'email']
      },
      {
        level: 2,
        delay_minutes: 15,
        targets: [{ type: 'schedule', id: 'secondary_oncall' }],
        channels: ['slack', 'sms', 'email']
      },
      {
        level: 3,
        delay_minutes: 30,
        targets: [
          { type: 'schedule', id: 'manager_oncall' },
          { type: 'user', id: 'eng_manager' }
        ],
        channels: ['slack', 'sms', 'phone', 'email']
      }
    ],
    repeat_interval_minutes: 60,
    max_repeats: 3
  },
  'critical': {
    name: 'Critical Escalation',
    levels: [
      {
        level: 1,
        delay_minutes: 0,
        targets: [
          { type: 'schedule', id: 'primary_oncall' },
          { type: 'schedule', id: 'secondary_oncall' }
        ],
        channels: ['slack', 'sms', 'phone']
      },
      {
        level: 2,
        delay_minutes: 5,
        targets: [
          { type: 'schedule', id: 'manager_oncall' },
          { type: 'user', id: 'eng_manager' },
          { type: 'user', id: 'cto' }
        ],
        channels: ['slack', 'sms', 'phone']
      }
    ],
    repeat_interval_minutes: 15,
    max_repeats: 10
  }
};
 
/**
 * Get current on-call user from schedule
 */
async function getOnCallUser(scheduleId) {
  // In production, this would query PagerDuty, OpsGenie, or internal schedule
  const schedules = {
    'primary_oncall': { user_id: 'user_001', name: 'Alice', phone: '+1111111111' },
    'secondary_oncall': { user_id: 'user_002', name: 'Bob', phone: '+2222222222' },
    'manager_oncall': { user_id: 'user_003', name: 'Carol', phone: '+3333333333' }
  };
  return schedules[scheduleId];
}
 
/**
 * Determine escalation policy based on alert
 */
function getEscalationPolicy(alert) {
  if (alert.severity === 'critical') {
    return ESCALATION_POLICIES['critical'];
  }
  return ESCALATION_POLICIES['default'];
}
 
/**
 * Create escalation state for tracking
 */
function createEscalationState(alert, policy) {
  return {
    alert_id: alert.id,
    policy_name: policy.name,
    current_level: 0,
    started_at: new Date().toISOString(),
    acknowledged: false,
    acknowledged_by: null,
    acknowledged_at: null,
    resolved: false,
    resolved_at: null,
    escalation_history: [],
    repeat_count: 0
  };
}
 
/**
 * Process escalation step
 */
async function processEscalation(state, alert, policy) {
  const currentLevel = policy.levels[state.current_level];
 
  if (!currentLevel) {
    // All levels exhausted, check for repeat
    if (state.repeat_count < policy.max_repeats) {
      state.current_level = 0;
      state.repeat_count++;
      state.escalation_history.push({
        action: 'repeat',
        repeat_number: state.repeat_count,
        timestamp: new Date().toISOString()
      });
      return processEscalation(state, alert, policy);
    }
    return { done: true, state };
  }
 
  // Resolve targets to actual users
  const notifications = [];
 
  for (const target of currentLevel.targets) {
    let user;
    if (target.type === 'schedule') {
      user = await getOnCallUser(target.id);
    } else if (target.type === 'user') {
      user = { user_id: target.id }; // Would fetch from user service
    }
 
    if (user) {
      for (const channel of currentLevel.channels) {
        notifications.push({
          user,
          channel,
          alert,
          escalation_level: currentLevel.level
        });
      }
    }
  }
 
  state.escalation_history.push({
    action: 'notify',
    level: currentLevel.level,
    targets: currentLevel.targets,
    channels: currentLevel.channels,
    timestamp: new Date().toISOString()
  });
 
  // Schedule next escalation
  const nextEscalation = {
    scheduled_for: new Date(Date.now() + currentLevel.delay_minutes * 60 * 1000).toISOString(),
    next_level: state.current_level + 1
  };
 
  return {
    done: false,
    notifications,
    nextEscalation,
    state
  };
}
 
/**
 * Handle acknowledgment
 */
function acknowledgeAlert(state, userId) {
  state.acknowledged = true;
  state.acknowledged_by = userId;
  state.acknowledged_at = new Date().toISOString();
  state.escalation_history.push({
    action: 'acknowledged',
    user_id: userId,
    timestamp: state.acknowledged_at
  });
  return state;
}
 
/**
 * Handle resolution
 */
function resolveAlert(state, userId) {
  state.resolved = true;
  state.resolved_at = new Date().toISOString();
  state.escalation_history.push({
    action: 'resolved',
    user_id: userId,
    timestamp: state.resolved_at
  });
  return state;
}
 
module.exports = {
  ESCALATION_POLICIES,
  getEscalationPolicy,
  createEscalationState,
  processEscalation,
  acknowledgeAlert,
  resolveAlert,
  getOnCallUser
};

Notification Templates

// notification-templates.js
/**
 * Notification templates for different scenarios.
 */
 
const templates = {
  // System alerts
  'system.high_cpu': {
    title: 'High CPU Usage Alert',
    message: 'Server {{server_name}} CPU usage is at {{cpu_percent}}%',
    severity: 'high',
    channels: ['slack', 'email']
  },
 
  'system.disk_space': {
    title: 'Low Disk Space Warning',
    message: 'Server {{server_name}} disk usage is at {{disk_percent}}% on {{mount_point}}',
    severity: 'medium',
    channels: ['slack', 'email']
  },
 
  'system.service_down': {
    title: 'Service Down',
    message: 'Service {{service_name}} is not responding on {{server_name}}',
    severity: 'critical',
    channels: ['slack', 'sms', 'pagerduty']
  },
 
  // Application alerts
  'app.error_rate': {
    title: 'High Error Rate',
    message: 'Application {{app_name}} error rate is {{error_rate}}% (threshold: {{threshold}}%)',
    severity: 'high',
    channels: ['slack', 'email']
  },
 
  'app.latency': {
    title: 'High Latency Detected',
    message: 'Application {{app_name}} p99 latency is {{latency_ms}}ms (threshold: {{threshold_ms}}ms)',
    severity: 'medium',
    channels: ['slack']
  },
 
  // Business alerts
  'business.payment_failure': {
    title: 'Payment Processing Failure',
    message: 'Payment processing failure rate is {{failure_rate}}% in the last {{time_window}}',
    severity: 'critical',
    channels: ['slack', 'sms', 'email', 'pagerduty']
  },
 
  'business.signup_anomaly': {
    title: 'Signup Anomaly Detected',
    message: 'Unusual signup activity detected: {{signup_count}} signups in {{time_window}} ({{percent_change}}% vs average)',
    severity: 'medium',
    channels: ['slack', 'email']
  },
 
  // Security alerts
  'security.auth_failure': {
    title: 'Authentication Failure Spike',
    message: 'Detected {{failure_count}} failed authentication attempts from IP {{source_ip}}',
    severity: 'high',
    channels: ['slack', 'email', 'security_team']
  },
 
  'security.suspicious_activity': {
    title: 'Suspicious Activity Detected',
    message: 'Suspicious activity detected: {{activity_description}} for user {{user_id}}',
    severity: 'high',
    channels: ['slack', 'email', 'security_team']
  }
};
 
/**
 * Render a template with variables
 */
function renderTemplate(templateId, variables) {
  const template = templates[templateId];
  if (!template) {
    throw new Error(`Unknown template: ${templateId}`);
  }
 
  let title = template.title;
  let message = template.message;
 
  // Replace variables
  for (const [key, value] of Object.entries(variables)) {
    const placeholder = `{{${key}}}`;
    title = title.replace(new RegExp(placeholder, 'g'), value);
    message = message.replace(new RegExp(placeholder, 'g'), value);
  }
 
  return {
    ...template,
    title,
    message,
    template_id: templateId,
    rendered_at: new Date().toISOString()
  };
}
 
/**
 * Validate template variables
 */
function validateTemplateVariables(templateId, variables) {
  const template = templates[templateId];
  if (!template) {
    return { valid: false, error: `Unknown template: ${templateId}` };
  }
 
  const requiredVars = [];
  const combinedText = template.title + template.message;
  const matches = combinedText.match(/\{\{(\w+)\}\}/g) || [];
 
  for (const match of matches) {
    const varName = match.replace(/[{}]/g, '');
    if (!requiredVars.includes(varName)) {
      requiredVars.push(varName);
    }
  }
 
  const missingVars = requiredVars.filter(v => !(v in variables));
 
  if (missingVars.length > 0) {
    return {
      valid: false,
      error: `Missing required variables: ${missingVars.join(', ')}`
    };
  }
 
  return { valid: true };
}
 
module.exports = {
  templates,
  renderTemplate,
  validateTemplateVariables
};

Conclusion

Building effective notification systems with n8n requires:

  1. Intelligent routing based on severity, user preferences, and time of day
  2. Alert aggregation to prevent notification fatigue from similar alerts
  3. Escalation policies ensuring critical alerts reach someone
  4. Multi-channel delivery with appropriate formatting for each channel
  5. Template management for consistent, informative notifications

These patterns create notification systems that are reliable, actionable, and respect users' attention while ensuring critical issues are never missed.

Weekly AI Security & Automation Digest

Get the latest on AI Security, workflow automation, secure integrations, and custom platform development delivered weekly.

No spam. Unsubscribe anytime.