n8n Automation

Automatizarea Notificarilor cu n8n: Proiectarea unui Sistem de Alerte Multi-Canal

Petru Constantin
--14 min lectura
#n8n#notifications#automation#alerting#slack#email#sms#webhooks

Automatizarea Notificarilor cu n8n: Proiectarea unui Sistem de Alerte Multi-Canal

Sistemele eficiente de notificari sunt esentiale pentru raspunsul la incidente, engagementul utilizatorilor si vizibilitatea operationala. Acest ghid acopera construirea de sisteme inteligente de notificari multi-canal cu n8n, care ruteaza alertele corespunzator si previn oboseala de notificari.

Arhitectura Notificarilor

Principii de Design

Un sistem de notificari bine proiectat trebuie sa:

  1. Ruteze inteligent - Trimite alertele catre oamenii potriviti prin canalele potrivite
  2. Previna oboseala - Agrega alertele similare si respecta orele de liniste
  3. Escaladeze corespunzator - Se asigura ca alertele critice ajung la cineva
  4. Urmareasca livrarea - Confirma ca notificarile sunt primite si confirmate

Router Multi-Canal

// 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
};

Workflow-ul de Agregare a Alertelor

{
  "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}]
      ]
    }
  }
}

Workflow-ul pentru Politica de Escaladare

// 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
};

Template-uri de Notificari

// 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
};

Concluzie

Construirea de sisteme eficiente de notificari cu n8n necesita:

  1. Rutare inteligenta bazata pe severitate, preferintele utilizatorilor si momentul zilei
  2. Agregarea alertelor pentru a preveni oboseala de notificari cauzata de alerte similare
  3. Politici de escaladare care asigura ca alertele critice ajung la cineva
  4. Livrare pe mai multe canale cu formatare potrivita fiecarui canal
  5. Gestionarea template-urilor pentru notificari consistente si informative

Aceste pattern-uri creeaza sisteme de notificari fiabile, actionabile, care respecta atentia utilizatorilor si in acelasi timp se asigura ca problemele critice nu sunt niciodata ratate.

Ai nevoie de ajutor cu conformitatea EU AI Act sau securitatea AI?

Programeaza o consultatie gratuita de 30 de minute. Fara obligatii.

Programeaza un Apel

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.