AutomationFlowsData & Sheets › Logsentinel Workflow

Logsentinel Workflow

LogSentinel Workflow. Uses postgres, emailSend, httpRequest. Webhook trigger; 44 nodes.

Webhook trigger★★★★★ complexity44 nodesPostgresEmail SendHTTP Request
Data & Sheets Trigger: Webhook Nodes: 44 Complexity: ★★★★★ Added:

This workflow follows the Emailsend → HTTP Request recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "name": "LogSentinel Workflow",
  "nodes": [
    {
      "parameters": {
        "schema": {
          "__rl": true,
          "mode": "list",
          "value": "public"
        },
        "table": {
          "__rl": true,
          "value": "logs",
          "mode": "list",
          "cachedResultName": "logs"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [
            "id"
          ],
          "schema": [
            {
              "id": "id",
              "displayName": "id",
              "required": false,
              "defaultMatch": true,
              "display": true,
              "type": "number",
              "canBeUsedToMatch": true,
              "removed": true
            },
            {
              "id": "source_id",
              "displayName": "source_id",
              "required": true,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "log_timestamp",
              "displayName": "log_timestamp",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "dateTime",
              "canBeUsedToMatch": true
            },
            {
              "id": "level",
              "displayName": "level",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": true
            },
            {
              "id": "category",
              "displayName": "category",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": true
            },
            {
              "id": "message",
              "displayName": "message",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "raw_log",
              "displayName": "raw_log",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "format_type",
              "displayName": "format_type",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "afd_state",
              "displayName": "afd_state",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": true
            },
            {
              "id": "afd_symbol",
              "displayName": "afd_symbol",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": true
            },
            {
              "id": "previous_state",
              "displayName": "previous_state",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": true
            },
            {
              "id": "llm_cache_hit",
              "displayName": "llm_cache_hit",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "boolean",
              "canBeUsedToMatch": true,
              "removed": true
            },
            {
              "id": "classified_at",
              "displayName": "classified_at",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "dateTime",
              "canBeUsedToMatch": true,
              "removed": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        -432,
        664
      ],
      "id": "020bf0b0-5c3b-4b74-b865-a34756319d25",
      "name": "Insert rows in a table",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const body = $json.body || $json;\n\nlet raw_log;\nlet source_id;\n\n// Si body est une string : Content-Type text/plain \u2192 le body EST le log\nif (typeof body === \"string\") {\n  raw_log   = body;\n  source_id = \"unknown\";\n} else {\n  // Content-Type application/json \u2192 body a les champs raw_log et source_id\n  raw_log   = body.raw_log   ?? null;\n  source_id = body.source_id || \"unknown\";\n}\n\nfunction detectFormat(log) {\n  if (!log || typeof log !== \"string\" || log.trim() === \"\") return \"unknown\";\n\n  // JSON : commence par {\n  if (log.trimStart().startsWith(\"{\")) return \"json\";\n\n  // Apache : commence par une IP suivie de \"METHOD\n  if (/^\\d+\\.\\d+\\.\\d+\\.\\d+.*\"(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)/.test(log)) return \"apache\";\n\n  // Syslog : Mois JJ HH:MM:SS hostname process[pid]:\n  if (/^[A-Z][a-z]{2}\\s+\\d{1,2}\\s+\\d{2}:\\d{2}:\\d{2}\\s+\\S+\\s+\\S+\\[/.test(log)) return \"syslog\";\n\n  // Plaintext : commence par une date YYYY-MM-DD\n  if (/^\\d{4}-\\d{2}-\\d{2}/.test(log)) return \"plaintext\";\n\n  return \"unknown\";\n}\n\nreturn {\n  raw_log,\n  source_id,\n  format_type: detectFormat(raw_log)\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -1104,
        760
      ],
      "id": "7bf2bb18-02b4-4600-922a-54aecbc9029a",
      "name": "Format Detector"
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "leftValue": "={{$json.format_type}}",
                    "rightValue": "json",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "id": "a567dbb2-09e4-4a0f-bb09-68436f7dd491"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "JSON Processor"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "id": "b10f9faa-df5a-4c60-81bc-8e9527ea522d",
                    "leftValue": "={{$json.format_type}}",
                    "rightValue": "syslog",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "Syslog Processor"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "id": "591c9d88-29f2-4a9b-b9e5-fc53137fac20",
                    "leftValue": "={{$json.format_type}}",
                    "rightValue": "apache",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "Apache Processor"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "id": "7359ff2b-2af0-4189-9e0f-b713fa7eec3a",
                    "leftValue": "={{$json.format_type}}",
                    "rightValue": "plaintext",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "Plaintext Processor"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "id": "0a645b9e-9940-4670-b75d-08b82fbf443d",
                    "leftValue": "={{$json.format_type}}",
                    "rightValue": "unknown",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "Unknown Processor"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.4,
      "position": [
        -880,
        712
      ],
      "id": "e2012df9-a25a-4a96-a2d6-f0b28bccd033",
      "name": "Switch"
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const rawStr = $json.raw_log;\nconst bodySourceId = $json.source_id || \"unknown\";\n\nlet log = {};\ntry {\n  log = typeof rawStr === \"string\" ? JSON.parse(rawStr) : {};\n} catch(e) { log = {}; }\n\n// Pr\u00e9fixer le message avec le level pour que les regex le capturent\nconst level   = log.level || \"\";\nconst message = log.message || JSON.stringify(log);\nconst fullMessage = level ? `${level}: ${message}` : message;\n\nreturn {\n  source_id:     log.service || log.source_id || bodySourceId,\n  log_timestamp: log.timestamp || new Date().toISOString(),\n  raw_log:       rawStr,\n  message:       fullMessage,\n  format_type:   \"json\"\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -656,
        256
      ],
      "id": "b60dd655-01b5-4924-857d-8eb917e9868f",
      "name": "JSON Processor"
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const log = $json.raw_log;\n\nconst match = typeof log === \"string\"\n  ? log.match(/^[A-Z][a-z]{2}\\s+\\d+\\s+\\d+:\\d+:\\d+\\s+\\S+\\s+(\\S+)\\[\\d+\\]:\\s+(.*)$/)\n  : null;\n\nreturn {\n  source_id: match?.[1] || $json.source_id || \"unknown\",\n  log_timestamp: new Date().toISOString(),\n  raw_log: log,\n  message: match?.[2] || log,\n  format_type: \"syslog\"\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -656,
        448
      ],
      "id": "b96933ef-fee4-4469-8160-d866e6c8638b",
      "name": "Syslog Processor"
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const log = $json.raw_log;\n\nconst match = typeof log === \"string\"\n  ? log.match(/(\\d+\\.\\d+\\.\\d+\\.\\d+).*?\\[(.*?)\\]\\s+\"(.*?)\"\\s+\\d+\\s+\\d+(.*)/)\n  : null;\n\n// match[3] = la requ\u00eate HTTP  (\"POST /api/checkout\")\n// match[4] = tout ce qui suit (CRITICAL: Database connection pool exhausted)\nconst requestPart  = match?.[3] || \"\";\nconst trailingPart = match?.[4]?.trim() || \"\";\n\nreturn {\n  source_id:     $json.source_id || \"apache\",\n  log_timestamp: match?.[2] || new Date().toISOString(),\n  raw_log:       log,\n  message:       trailingPart\n                   ? `${requestPart} ${trailingPart}`\n                   : (requestPart || log),\n  format_type:   \"apache\"\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -656,
        640
      ],
      "id": "c901f10c-306b-4e76-a3ae-d67ff8c9c0a2",
      "name": "Apache Processor"
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const log = $json.raw_log;\n\nconst parts = typeof log === \"string\" ? log.split(\" \") : [];\n\nreturn {\n  source_id: $json.source_id || \"unknown\",\n  log_timestamp: parts.slice(0, 2).join(\" \") || new Date().toISOString(),\n  raw_log: log,\n  message: parts.slice(2).join(\" \") || log,\n  format_type: \"text\"\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -656,
        832
      ],
      "id": "642fb2ab-663c-4cb5-9dfe-8d73a1a682e5",
      "name": "Plaintext Processor"
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "return {\n  source_id: $json.source_id || \"unknown\",\n  log_timestamp: new Date().toISOString(),\n  raw_log: $json.raw_log,\n  message: $json.raw_log,\n  format_type: \"unknown\"\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -656,
        1024
      ],
      "id": "5c8815df-149b-4656-a728-18338e4457a3",
      "name": "Unknown Processor"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT\n  COALESCE(s.current_state, 'INFO')   AS current_state,\n  COALESCE(s.cascade_count, 0)        AS cascade_count,\n  s.last_event_time IS NULL           AS is_new_service,\n  COALESCE(\n    EXTRACT(EPOCH FROM (NOW() - s.last_event_time))::int,\n    999999\n  )                                   AS elapsed_since_last_sec,\n  COALESCE(\n    EXTRACT(EPOCH FROM (NOW() - s.cascade_start_time))::int,\n    999999\n  )                                   AS cascade_elapsed_sec\nFROM (SELECT 1) AS dummy\nLEFT JOIN afd_states s\n  ON s.source_id = '{{ $json.source_id }}'\n AND '{{ $json.source_id }}' <> ''",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        -208,
        664
      ],
      "id": "5660c632-b83c-49e8-aa3f-f2acbaaf0aea",
      "name": "AFD State Read",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// 0. CONFIG\nconst TIMEOUT_WINDOW_SEC  = parseInt($env.TIMEOUT_WINDOW    ?? '300', 10);\nconst CASCADE_THRESHOLD   = parseInt($env.CASCADE_THRESHOLD ?? '3',   10);\nconst CASCADE_WINDOW_SEC  = parseInt($env.CASCADE_WINDOW    ?? '60',  10);\n\nconst EFFECTIVE_TIMEOUT   = Number.isFinite(TIMEOUT_WINDOW_SEC)  && TIMEOUT_WINDOW_SEC  > 0 ? TIMEOUT_WINDOW_SEC  : 300;\nconst EFFECTIVE_THRESHOLD = Number.isFinite(CASCADE_THRESHOLD)   && CASCADE_THRESHOLD   > 0 ? CASCADE_THRESHOLD   : 3;\nconst EFFECTIVE_CASCADE   = Number.isFinite(CASCADE_WINDOW_SEC)  && CASCADE_WINDOW_SEC  > 0 ? CASCADE_WINDOW_SEC  : 60;\n\n// 1. TRANSITION TABLE 6\u00d76\nconst TRANSITIONS = {\n  INFO:          { sigma_info:'INFO',     sigma_warn:'WARNING',  sigma_error:'ERROR',    sigma_crit:'CRITICAL', sigma_ack:'INFO',     sigma_malformed:'INFO',          sigma_timeout:'INFO'  },\n  WARNING:       { sigma_info:'INFO',     sigma_warn:'WARNING',  sigma_error:'ERROR',    sigma_crit:'CRITICAL', sigma_ack:'WARNING',  sigma_malformed:'WARNING',       sigma_timeout:'INFO'  },\n  ERROR:         { sigma_info:'WARNING',  sigma_warn:'WARNING',  sigma_error:'ERROR',    sigma_crit:'CRITICAL', sigma_ack:'ERROR',    sigma_malformed:'ERROR',         sigma_timeout:'INFO'  },\n  ERROR_CASCADE: { sigma_info:'WARNING',  sigma_warn:'ERROR',    sigma_error:'ERROR',    sigma_crit:'CRITICAL', sigma_ack:'ERROR',    sigma_malformed:'ERROR_CASCADE', sigma_timeout:'INFO'  },\n  CRITICAL:      { sigma_info:'CRITICAL', sigma_warn:'CRITICAL', sigma_error:'CRITICAL', sigma_crit:'CRITICAL', sigma_ack:'RECOVERY', sigma_malformed:'CRITICAL',      sigma_timeout:'INFO'  },\n  RECOVERY:      { sigma_info:'INFO',     sigma_warn:'WARNING',  sigma_error:'ERROR',    sigma_crit:'CRITICAL', sigma_ack:'RECOVERY', sigma_malformed:'RECOVERY',      sigma_timeout:'INFO'  },\n};\n\n// 2. REGEX PATTERNS\nconst REGEX_PATTERNS = [\n  { symbol: 'sigma_crit',  pattern: /\\b(CRITICAL|FATAL|EMERGENCY|PANIC|EMERG|KERN_CRIT|SEV1|P1)\\b/i },\n  { symbol: 'sigma_error', pattern: /\\b(ERROR|Exception|Traceback|FAILED|FAILURE|5[0-9]{2}|NullPointer|OutOfMemory|Segfault|SIGSEGV|SIGABRT)\\b/i },\n  { symbol: 'sigma_warn',  pattern: /\\b(WARN|WARNING|SLOW|RETRY|DEPRECATED|TIMEOUT(?!_WINDOW)|HIGH_LATENCY|BACKPRESSURE|THROTTL)\\b/i },\n  { symbol: 'sigma_info',  pattern: /\\b(INFO|DEBUG|TRACE|OK|SUCCESS|STARTED|STOPPED|HEALTHY|2[0-9]{2}|CONNECTED|INITIALIZED)\\b/i },\n];\nconst ACK_PATTERN = /\\b(__AFD_ACK__)\\b/;\n\nfunction evaluateSymbol(message) {\n  if (message === null || message === undefined) return 'sigma_malformed';\n  if (typeof message !== 'string' || message.trim() === '') return 'sigma_malformed';\n  if (ACK_PATTERN.test(message)) return 'sigma_ack';\n  for (const { symbol, pattern } of REGEX_PATTERNS) {\n    if (pattern.test(message)) return symbol;\n  }\n  return 'sigma_malformed';\n}\n\nfunction applyTransition(state, symbol) {\n  const row = TRANSITIONS[state];\n  if (!row) return TRANSITIONS['INFO'][symbol] ?? 'INFO';\n  return row[symbol] ?? state;\n}\n\n// 3. READ LOG\nconst logObject = $('Insert rows in a table').item.json;\nconst sourceId  = logObject.source_id ?? 'unknown';\nconst message   = logObject.message   ?? '';\nconst logId     = logObject.id        ?? null;\nconst now       = new Date();\n\n// 4. READ AFD STATE\nconst stateRow = $json;\n\nlet rawState = stateRow?.current_state ?? 'INFO';\nif (rawState === 'NOMINAL')  rawState = 'INFO';\nif (rawState === 'DEGRADED') rawState = 'WARNING';\nlet currentState = TRANSITIONS[rawState] ? rawState : 'INFO';\n\nconst isNewService      = stateRow?.is_new_service !== false;\nconst cascadeCount      = parseInt(stateRow?.cascade_count ?? '0', 10) || 0;\nconst elapsedSec        = Number(stateRow?.elapsed_since_last_sec ?? 999999);\nconst cascadeElapsedSec = Number(stateRow?.cascade_elapsed_sec   ?? 999999);\n\n// 5. TEMPORAL RESET\nlet stateAfterTimeout = currentState;\nlet timeoutApplied    = false;\nlet newCascadeCount   = cascadeCount;\n\nif (!isNewService && elapsedSec > EFFECTIVE_TIMEOUT) {\n  stateAfterTimeout = applyTransition(currentState, 'sigma_timeout');\n  timeoutApplied    = true;\n  newCascadeCount   = 0;\n}\n\n// 6. SYMBOL + TRANSITION + CASCADE WINDOW\nconst symbol      = evaluateSymbol(message);\nconst rawNewState = applyTransition(stateAfterTimeout, symbol);\n\nlet newState      = rawNewState;\nlet cascade_reset = 'keep';\n\nif (rawNewState === 'ERROR') {\n  if (cascadeElapsedSec > EFFECTIVE_CASCADE) {\n    newCascadeCount = 1;\n    cascade_reset   = 'reset_now';\n    newState        = 'ERROR';\n  } else {\n    newCascadeCount = cascadeCount + 1;\n    cascade_reset   = 'keep';\n    if (newCascadeCount >= EFFECTIVE_THRESHOLD) {\n      newState = 'ERROR_CASCADE';\n    }\n  }\n} else if (rawNewState === 'ERROR_CASCADE') {\n  newCascadeCount = cascadeCount + 1;\n  cascade_reset   = 'keep';\n} else {\n  if (currentState === 'ERROR' || currentState === 'ERROR_CASCADE') {\n    newCascadeCount = 0;\n    cascade_reset   = 'reset_null';\n  }\n}\n\nconst previousState = currentState;\n\n// 7. OUTPUT\nreturn {\n  ...logObject,\n  afd_symbol:        symbol,\n  afd_state:         newState,\n  previous_state:    previousState,\n  source_id:         sourceId,\n  id:                logId,\n  cascade_count:     newCascadeCount,\n  cascade_reset:     cascade_reset,\n  is_new_service:    isNewService,\n  timeout_applied:   timeoutApplied,\n  elapsed_sec:       Math.round(elapsedSec),\n  cascade_elapsed:   Math.round(cascadeElapsedSec),\n  cascade_threshold: EFFECTIVE_THRESHOLD,\n  cascade_window:    EFFECTIVE_CASCADE,\n  engine_ts:         now.toISOString(),\n  engine_version:    'v2',\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        16,
        664
      ],
      "id": "c6e07d8d-e194-46b1-9f04-7375d0d64232",
      "name": "AFD Engine"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO afd_states (\n  source_id, current_state, previous_state, last_symbol,\n  last_event_time, cascade_count, cascade_start_time, updated_at\n)\nVALUES (\n  '{{ $json.source_id }}',\n  '{{ $json.afd_state }}',\n  '{{ $json.previous_state }}',\n  '{{ $json.afd_symbol }}',\n  NOW(),\n  {{ $json.cascade_count }},\n  CASE '{{ $json.cascade_reset }}'\n    WHEN 'reset_now' THEN NOW()\n    ELSE NULL\n  END,\n  NOW()\n)\nON CONFLICT (source_id) DO UPDATE SET\n  current_state      = EXCLUDED.current_state,\n  previous_state     = EXCLUDED.previous_state,\n  last_symbol        = EXCLUDED.last_symbol,\n  last_event_time    = NOW(),\n  cascade_count      = EXCLUDED.cascade_count,\n  cascade_start_time = CASE '{{ $json.cascade_reset }}'\n    WHEN 'reset_now'  THEN NOW()\n    WHEN 'reset_null' THEN NULL\n    ELSE afd_states.cascade_start_time\n  END,\n  updated_at         = NOW()",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        240,
        568
      ],
      "id": "d257c23d-ad18-4dd3-9893-99de30ee6932",
      "name": "AFD State Write",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "UPDATE logs SET afd_state = '{{ $json.afd_state }}', afd_symbol = '{{ $json.afd_symbol }}', previous_state = '{{ $json.previous_state }}' WHERE id = {{ $json.id }}",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        240,
        760
      ],
      "id": "45d058cf-dec0-48a6-90c8-ea3cee6a1811",
      "name": "Log State Write",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "id": "ab6ff2e0-ca5f-4b91-8970-f46690fb1d74",
                    "leftValue": "={{ ['INFO', 'NOMINAL'].includes($json.afd_state) }}",
                    "rightValue": "true",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "INFO Handler"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "id": "710fdff7-1a07-4f1a-aeea-f9404a85c721",
                    "leftValue": "={{ ['WARNING', 'DEGRADED'].includes($json.afd_state) }}",
                    "rightValue": "",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "WARNING Handler"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "id": "22be804b-0965-4e3a-adca-ab0d690c5965",
                    "leftValue": "={{$json.afd_state}}",
                    "rightValue": "ERROR",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "ERROR Handler"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "id": "9d97fd1e-49d1-401b-98ac-618d1b463c50",
                    "leftValue": "={{ $json.afd_state }}",
                    "rightValue": "RECOVERY",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "RECOVERY Handler"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "id": "ff408d4f-2f53-4e9a-bbfc-7d9bfa22e67d",
                    "leftValue": "={{ ['CRITICAL', 'ERROR_CASCADE'].includes($json.afd_state) }}",
                    "rightValue": "",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "CRITICAL Handler"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.4,
      "position": [
        240,
        280
      ],
      "id": "fc96383f-9471-4c62-a3f2-a7b21633786c",
      "name": "AFD State Switch Router"
    },
    {
      "parameters": {
        "fromEmail": "={{$env.SMTP_FROM}}",
        "toEmail": "={{$env.SMTP_TO}}",
        "subject": "=WARNING - {{$json.source_id}}",
        "emailFormat": "text",
        "text": "=Warning detected:\n{{$json.message}}\n\nState: WARNING\nTime: {{$now}}",
        "options": {}
      },
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        464,
        -32
      ],
      "id": "ac57f744-7aa5-440e-b173-48b76848e244",
      "name": "Send an Email",
      "credentials": {
        "smtp": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "fromEmail": "={{$env.SMTP_FROM}}",
        "toEmail": "={{$env.SMTP_TO}}",
        "subject": "=ERROR - {{$json.source_id}}",
        "emailFormat": "text",
        "text": "=Error detected:\n{{$json.message}}\n\nPlease investigate immediately.",
        "options": {}
      },
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        464,
        352
      ],
      "id": "8fe4de58-6c24-45f8-b43f-28602c20bd45",
      "name": "Send an Email1",
      "credentials": {
        "smtp": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "mode": "raw",
        "jsonOutput": "{\n  \"status\": \"WARNING\",\n  \"message\": \"email_sent\"\n}",
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        464,
        160
      ],
      "id": "f54d803f-8852-4a78-a310-7f40eeda7ada",
      "name": "WARNING Message"
    },
    {
      "parameters": {
        "mode": "raw",
        "jsonOutput": "{\n  \"status\": \"INFO\",\n  \"message\": \"processed\"\n}",
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        464,
        -224
      ],
      "id": "6090ebf2-f907-4655-9b93-4936354400eb",
      "name": "INFO Message"
    },
    {
      "parameters": {
        "mode": "raw",
        "jsonOutput": "{\n  \"status\": \"ERROR\",\n  \"message\": \"email_sent\"\n}",
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        464,
        544
      ],
      "id": "ce19b482-2c50-4fa1-b343-36d6461f9db2",
      "name": "ERROR Message"
    },
    {
      "parameters": {
        "mode": "raw",
        "jsonOutput": "{\n  \"status\": \"CRITICAL\",\n  \"message\": \"sent_to_llm\"\n}",
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        464,
        928
      ],
      "id": "09e3a44d-451a-48b5-abc3-1d2779b0ccae",
      "name": "CRITICAL Message"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={\n  \"status\": \"{{$json.status}}\",\n  \"message\": \"{{$json.message}}\"\n}",
        "options": {
          "responseCode": 200
        }
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        688,
        352
      ],
      "id": "ac744d7f-97e3-429e-b949-c6a2547e9867",
      "name": "Final Response"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "logs",
        "responseMode": "responseNode",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        -1328,
        760
      ],
      "id": "e36483f8-796d-4c05-81db-824d57e890fb",
      "name": "Webhook1"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT message \nFROM logs WHERE source_id = '{{ $json.source_id }}'\nORDER BY log_timestamp DESC\nLIMIT 5;\n",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        912,
        1216
      ],
      "id": "f270d84f-9924-46b4-9220-c1192de7578d",
      "name": "Get Recent Logs",
      "alwaysOutputData": true,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const originalLog = $('AFD Engine').first().json;\nconst rows = $input.all();\nconst recent_logs = rows.map(r => r.json.message);\n\nreturn [{\n  json: {\n    ...originalLog,\n    recent_logs\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1136,
        1216
      ],
      "id": "f90d7c90-ffbf-4f90-bc74-d48a7cefb8ec",
      "name": "Format Recent Logs"
    },
    {
      "parameters": {
        "jsCode": "const log = $input.first().json;\n\nconst actions = Array.isArray(log.actions_immediates)\n  ? log.actions_immediates\n  : [];\n\nconst timestamp = new Date().toISOString();\n\n/**\n * Format all actions\n */\nconst actionsText = actions.length\n  ? actions.map((a, i) => `${i + 1}. ${a}`).join('\\n')\n  : \"N/A\";\n\n/**\n * WhatsApp Message \n */\nconst whatsappMessage = `\nCRITICAL \u2014 ${log.source_id}\n\n${log.message?.slice(0, 200)}\n\nCause probable:\n${log.cause_probable}\n\nActions:\n${actionsText}\n\n${timestamp}\n\nReply:\nACK \u2014 Prise en charge\nFALSE-POSITIVE \u2014 Faux positif\nCORRIGER-WARNING \u2014 Reclassifier warning\nCORRIGER-ERROR \u2014 Reclassifier error\nRESOLVED \u2014 Incident r\u00e9solu\nSTATUS \u2014 \u00c9tat du syst\u00e8me\nESCALATE \u2014 Escalader\nHELP \u2014 Aide\n`;\n\n/**\n * HTML Email (no emojis)\n */\nconst htmlMessage = `\n<h2>CRITICAL Alert</h2>\n\n<p><b>Service:</b> ${log.source_id}</p>\n\n<p><b>Message:</b><br>${log.message}</p>\n\n<p><b>Cause probable:</b><br>${log.cause_probable}</p>\n\n<p><b>Actions:</b></p>\n<ul>\n${actions.map(a => `<li>${a}</li>`).join('')}\n</ul>\n\n<p><b>Timestamp:</b> ${timestamp}</p>\n`;\n\nreturn [\n  {\n    json: {\n      ...log,\n      whatsappMessage,\n      htmlMessage,\n      alert_timestamp: timestamp\n    }\n  }\n];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2704,
        1024
      ],
      "id": "a02e5147-0ba2-49ea-9598-984eaa4cc01d",
      "name": "Format Alert Message"
    },
    {
      "parameters": {
        "fromEmail": "={{$env.SMTP_FROM}}",
        "toEmail": "={{$env.SMTP_TO}}",
        "subject": "=[CRITICAL] {{$json.source_id}} \u2014 {{$json.alert_timestamp}}",
        "html": "={{$json.htmlMessage}}",
        "options": {}
      },
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        2928,
        1024
      ],
      "id": "c4ebafc7-be43-482b-bc83-cb044e031fb9",
      "name": "Alert Email",
      "credentials": {
        "smtp": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "UPDATE afd_states\nSET ack_time = NULL\nWHERE source_id = '{{$json.source_id}}';",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        2928,
        1216
      ],
      "id": "57b52d44-56be-4fa6-a86e-ba8217dc0fa0",
      "name": "Reset ACK Time",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://api.twilio.com/2010-04-01/Accounts/{{$env.TWILIO_ACCOUNT_SID}}/Messages.json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "sendBody": true,
        "contentType": "form-urlencoded",
        "bodyParameters": {
          "parameters": [
            {
              "name": "From",
              "value": "={{$env.TWILIO_WHATSAPP_FROM}}"
            },
            {
              "name": "To",
              "value": "={{$env.TWILIO_WHATSAPP_ADMIN}}"
            },
            {
              "name": "Body",
              "value": "={{$json.whatsappMessage}}"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        2928,
        832
      ],
      "id": "5349fe24-c7f2-472e-87cd-684b862788de",
      "name": "Twilio WhatsApp Alert",
      "credentials": {
        "httpBasicAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT COUNT(*)::int AS critical_count\nFROM afd_states\nWHERE current_state = 'CRITICAL';",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        464,
        1120
      ],
      "id": "172155c9-06aa-46be-8ffe-84b578af774e",
      "name": "Count Critical Services",
      "alwaysOutputData": true,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "d1b82296-d5eb-4508-acdf-863762ba3966",
              "leftValue": "={{$json.critical_count >= 2}}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        688,
        1120
      ],
      "id": "36961dc3-b17e-4d8d-97aa-7aab47cb2615",
      "name": "System Wide Critical?",
      "alwaysOutputData": false
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT\n    source_id,\n    current_state,\n    last_event_time\nFROM afd_states\nWHERE current_state = 'CRITICAL'\nORDER BY last_event_time DESC;",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        1808,
        784
      ],
      "id": "e3fcfc2c-8690-4035-b804-be583779d880",
      "name": "Get All Critical Services",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const services = $input.all().map(item => ({\n  source_id: item.json.source_id,\n  state: item.json.current_state,\n  last_event_time: item.json.last_event_time\n}));\n\nconst prompt = `\nYou are an expert SRE incident correlation engine.\n\nMultiple services are simultaneously in CRITICAL state.\n\nAnalyze possible correlations and shared root causes.\n\nServices:\n${JSON.stringify(services, null, 2)}\n\nRespond ONLY in JSON:\n{\n  \"global_incident\": true,\n  \"possible_root_cause\": \"...\",\n  \"recommended_action\": \"...\",\n  \"severity\": \"HIGH\"\n}\n`;\n\nreturn [{\n  json: {\n    services,\n    llm_payload: {\n      model: $env.GROQ_MODEL ?? 'llama-3.1-8b-instant',\n      messages: [\n        {\n          role: 'user',\n          content: prompt\n        }\n      ],\n      temperature: 0.2,\n      max_tokens: 250,\n      response_format: {\n        type: \"json_object\"\n      }\n    }\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2032,
        784
      ],
      "id": "b0ac7166-17c4-41b9-b809-97e6dceb61f6",
      "name": "Build Correlation Prompt"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{$env.GROQ_API_URL}}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{$json.llm_payload}}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        2256,
        784
      ],
      "id": "fc8e7cbf-f2d8-4e74-94d4-97a6c3c85ca5",
      "name": "Groq Correlation Call",
      "credentials": {
        "httpBearerAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const httpResult = $json;\n\nif (httpResult.error || !httpResult.choices) {\n  return [{\n    json: {\n      global_incident: true,\n      possible_root_cause: 'Unable to determine root cause',\n      recommended_action: 'Manual investigation required',\n      severity: 'HIGH',\n      llm_source: 'fallback'\n    }\n  }];\n}\n\ntry {\n  const rawText = httpResult.choices[0].message.content.trim();\n  const parsed = JSON.parse(rawText);\n\n  return [{\n    json: {\n      global_incident: parsed.global_incident ?? true,\n      possible_root_cause: parsed.possible_root_cause,\n      recommended_action: parsed.recommended_action,\n      severity: parsed.severity ?? 'HIGH',\n      llm_source: 'groq'\n    }\n  }];\n} catch (e) {\n  return [{\n    json: {\n      global_incident: true,\n      possible_root_cause: 'Correlation parsing failed',\n      recommended_action: 'Investigate all affected services',\n      severity: 'HIGH',\n      llm_source: 'fallback'\n    }\n  }];\n}"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2480,
        784
      ],
      "id": "20b730e3-0ae4-46ff-b0d6-a0fe8c8863fd",
      "name": "Parse Correlation Response"
    },
    {
      "parameters": {
        "mode": "raw",
        "jsonOutput": "={\n  \"whatsappMessage\": \"SYSTEM-WIDE CRITICAL\\n\\nMultiple services are simultaneously failing.\\n\\nPossible root cause:\\n{{$json.possible_root_cause}}\\n\\nRecommended action:\\n{{$json.recommended_action}}\"\n}",
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        2704,
        784
      ],
      "id": "91791b61-7f2e-4007-9183-de26f15ed293",
      "name": "System Wide Alert Message"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT\n    EXISTS (\n        SELECT 1\n        FROM llm_cache\n        WHERE message_hash = '{{ $json.message_hash }}'\n    ) AS cache_found,\n    (\n        SELECT response_text\n        FROM llm_cache\n        WHERE message_hash = '{{ $json.message_hash }}'\n        LIMIT 1\n    ) AS response_text,\n    (\n        SELECT llm_source\n        FROM llm_cache\n        WHERE message_hash = '{{ $json.message_hash }}'\n        LIMIT 1\n    ) AS llm_source,\n    (\n        SELECT hit_count\n        FROM llm_cache\n        WHERE message_hash = '{{ $json.message_hash }}'\n        LIMIT 1\n    ) AS hit_count;",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        1584,
        1216
      ],
      "id": "df98f7fb-7203-49e0-ab1a-7c2a5f40666e",
      "name": "Check LLM Cache",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "83cd8edb-9bc2-4e12-922e-60adb18e763c",
              "leftValue": "={{ $json.response_text }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        1808,
        1216
      ],
      "id": "2828523c-7d97-43c3-b96e-5f42ec6e7c25",
      "name": "Cache Found?"
    },
    {
      "parameters": {
        "jsCode": "const originalLog = $('Build LLM Cache Key').first().json;\nconst cacheRow = $input.first().json;\n\nlet response;\n\ntry {\n  response = JSON.parse(cacheRow.response_text);\n} catch (e) {\n  response = {\n    cause_probable: cacheRow.response_text,\n    actions_immediates: ['V\u00e9rifier le service concern\u00e9 et consulter les logs r\u00e9cents.'],\n    escalade_si: 'Si le probl\u00e8me persiste',\n    severite_estimee: 'CRITICAL'\n  };\n}\n\nreturn [{\n  json: {\n    ...originalLog,\n    ...response,\n    llm_source: cacheRow.llm_source || 'cache',\n    llm_cache_hit: true\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2480,
        976
      ],
      "id": "2cfc1ad3-8b55-47d5-b37b-9461bb76363b",
      "name": "Use Cached LLM Response"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "UPDATE llm_cache\nSET\n    hit_count = hit_count + 1,\n    last_hit_at = NOW()\nWHERE message_hash = '{{ $json.message_hash }}';",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        2704,
        1216
      ],
      "id": "fbb352c6-a66c-41ca-918b-d93e23a1730e",
      "name": "Update LLM Cache Hit",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO llm_cache (\n    message_hash,\n    category,\n    afd_state,\n    response_text,\n    llm_source,\n    created_at,\n    last_hit_at,\n    hit_count\n)\nVALUES (\n    '{{ $json.message_hash }}',\n    '{{ $json.category }}',\n    '{{ $json.afd_state }}',\n    '{{ JSON.stringify({\n        cause_probable: $json.cause_probable,\n        actions_immediates: $json.actions_immediates,\n        escalade_si: $json.escalade_si,\n        severite_estimee: $json.severite_estimee\n    }).replace(/'/g, \"''\") }}',\n    '{{ $json.llm_source }}',\n    NOW(),\n    NOW(),\n    1\n)\nON CONFLICT (message_hash)\nDO UPDATE SET\n    last_hit_at = NOW(),\n    hit_count = llm_cache.hit_count + 1;",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        2704,
        1408
      ],
      "id": "f0ff6fef-55ba-4b37-b968-dd924b0ef916",
      "name": "Store LLM Cache",
      "alwaysOutputData": true,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const log = $('Build LLM Prompt').first().json;\n\nconst staticFallbacks = {\n  SECURITY:     'V\u00e9rifier les logs d\\'acc\u00e8s, bloquer l\\'IP source, notifier l\\'\u00e9quipe s\u00e9curit\u00e9.',\n  PERFORMANCE:  'V\u00e9rifier CPU/RAM/disque, identifier le processus fautif, red\u00e9marrer si n\u00e9cessaire.',\n  AVAILABILITY: 'V\u00e9rifier la connectivit\u00e9 r\u00e9seau, l\\'\u00e9tat du service et de ses d\u00e9pendances.',\n  DATA:         'V\u00e9rifier l\\'int\u00e9grit\u00e9 des donn\u00e9es, stopper les \u00e9critures, alerter l\\'\u00e9quipe DBA.',\n  NETWORK:      'V\u00e9rifier la connectivit\u00e9, les r\u00e8gles firewall, et les DNS.',\n  SYSTEM:       'V\u00e9rifier les ressources syst\u00e8me, les logs kernel, et l\\'espace disque.'\n};\n\nfunction staticResponse(category) {\n  return {\n    cause_probable:     'LLM indisponible \u2014 r\u00e9ponse statique',\n    actions_immediates: [staticFallbacks[category] || 'Analyser les logs et escalader.'],\n    escalade_si:        'Si le probl\u00e8me persiste plus de 10 minutes',\n    severite_estimee:   'CRITICAL',\n    llm_source:         'static'\n  };\n}\n\n// $json ici = r\u00e9ponse du HTTP Request node (ou objet erreur si \"Continue on Fail\")\nconst httpResult = $json;\n\n// Si le n\u0153ud HTTP a \u00e9chou\u00e9, $json contiendra un champ \"error\"\nif (httpResult.error || !httpResult.choices) {\n  return [{ json: { ...log, ...staticResponse(log.category) } }];\n}\n\ntry {\n  const rawText  = httpResult.choices[0].message.content.trim();\n  const parsed   = JSON.parse(rawText);\n\n  return [{\n    json: {\n      ...log,\n      cause_probable:     parsed.cause_probable,\n      actions_immediates: parsed.actions_immediates,\n      escalade_si:        parsed.escalade_si,\n      severite_estimee:   parsed.severite_estimee,\n      llm_source:         'groq'\n    }\n  }];\n} catch (e) {\n  return [{ json: { ...log, ...staticResponse(log.category) } }];\n}"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2480,
        1360
      ],
      "id": "711697cc-d1ef-4be5-9e7d-1d1b1d8c4009",
      "name": "Parse LLM Response1"
    },
    {
      "parameters": {
        "jsCode": "function simpleHash(input) {\n  let hash1 = 0x811c9dc5;\n  let hash2 = 0x01000193;\n\n  for (let i = 0; i < input.length; i++) {\n    const char = input.charCodeAt(i);\n\n    hash1 ^= char;\n    hash1 = Math.imul(hash1, 16777619);\n\n    hash2 ^= char;\n    hash2 = Math.imul(hash2, 2166136261);\n  }\n\n  const part1 = (hash1 >>> 0).toString(16).padStart(8, '0');\n  const part2 = (hash2 >>> 0).toString(16).padStart(8, '0');\n\n  return (part1 + part2 + part1 + part2 + part1 + part2 + part1 + part2).substring(0, 64);\n}\n\nreturn items.map(item => {\n  const log = item.json;\n\n  const message = String(log.message ?? log.raw_log ?? '');\n\n  const category = String(log.category ?? 'SYSTEM').toUpperCase();\n\n  const afdState = String(log.afd_state ?? 'CRITICAL').toUpperCase();\n\n  const normalizedMessage = message\n    .toLowerCase()\n    .replace(/\\d{4}-\\d{2}-\\d{2}t?\\s?\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?z?/gi, '<ts>')\n    .replace(/\\d{4}-\\d{2}-\\d{2}\\s+\\d{2}:\\d{2}:\\d{2}/g, '<ts>')\n    .replace(/\\b\\d{1,3}(\\.\\d{1,3}){3}\\b/g, '<ip>')\n    .replace(/:\\d{4,5}\\b/g, ':<port>')\n    .replace(/\\s+/g, ' ')\n    .trim()\n    .substring(0, 256);\n\n  const hashInput = `${normalizedMessage}|${category}|${afdState}`;\n  const messageHash = simpleHash(hashInput);\n\n  return {\n    json: {\n      ...log,\n      category: category,\n      afd_state: afdState,\n      normalized_message: normalizedMessage,\n      message_hash: messageHash,\n      llm_cache_hit: false\n    }\n  };\n});"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1360,
        1216
      ],
      "id": "7b6a66fd-6a43-48cf-8c9a-f21043e630a8",
      "name": "Build LLM Cache Key"
    },
    {
      "parameters": {
        "jsCode": "const log = $('Build LLM Cache Key').first().json;\n\nconst userPrompt = `Tu es un ing\u00e9nieur SRE expert en monitoring.\nUn log CRITICAL a \u00e9t\u00e9 d\u00e9tect\u00e9 dans le service \"${log.source_id}\".\nLog : ${log.message}\nCat\u00e9gorie : ${log.category || 'SYSTEM'}\nHistorique r\u00e9cent :\n${JSON.stringify(log.recent_logs || [])}\n\nR\u00e9ponds UNIQUEMENT en JSON valide, sans texte avant ou apr\u00e8s :\n{\n  \"cause_probable\": \"...\",\n  \"actions_immediates\": [\"action 1\", \"action 2\", \"action 3\"],\n  \"escalade_si\": \"...\",\n  \"severite_estimee\": \"HIGH\"\n}\nMaximum 150 mots.`;\n\nreturn [{\n  json: {\n    ...log,\n    llm_payload: {\n      model: $env.GROQ_MODEL ?? 'llama-3.1-8b-instant',\n      messages: [{ role: 'user', content: userPrompt }],\n      max_tokens: 300,\n      temperature: 0.3,\n      response_format: { type: \"json_object\" }\n    }\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2032,
        1360
      ],
      "id": "841455c3-8cfb-43ab-83f5-97e8c8a8e5d9",
      "name": "Build LLM Prompt"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{$env.GROQ_API_URL}}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{$json.llm_payload}}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        2256,
        1360
      ],
      "id": "172ab3ed-1ddb-45a7-9441-f93b21ea5140",
      "name": "Groq API Call",
      "credentials": {
        "httpBearerAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "mode": "raw",
        "jsonOutput": "{\n  \"status\": \"RECOVERY\",\n  \"message\": \"monitoring_post_incident\"\n}",
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        464,
        736
      ],
      "id": "729b943a-90b4-414c-9fb5-5a9f9488a43a",
      "name": "RECOVERY Message"
    }
  ],
  "connections": {
    "Format Detector": {
      "main": [
        [
          {
            "node": "Switch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Switch": {
      "main": [
        [
          {
            "node": "JSON Processor",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Syslog Processor",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Apache Processor",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Plaintext Processor",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Unknown Processor",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JSON Processor": {
      "main": [
        [
          {
            "node": "Insert rows in a table",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Syslog Processor": {
      "main": [
        [
          {
            "node": "Insert rows in a table",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Apache Processor": {
      "main": [
        [
          {
            "node": "Insert rows in a table",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Plaintext Processor": {
      "main": [
        [
          {
            "node": "Insert rows in a table",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Unknown Processor": {
      "main": [
        [
          {
            "node": "Insert rows in a table",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Insert rows in a table": {
      "main": [
        [
          {
            "node": "AFD State Read",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AFD State Read": {
      "main": [
        [
          {
            "node": "AFD Engine",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AFD Engine": {
      "main": [
        [
          {
            "node": "AFD State Write",
            "type": "main",
            "index": 0
          },
          {
            "node": "Log State Write",
            "type": "main",
            "index": 0
          },
          {
            "node": "AFD State Switch Router",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AFD State Switch Router": {
      "main": [
        [
          {
            "node": "INFO Message",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send an Email",
            "type": "main",
            "index": 0
          },
          {
            "node": "WARNING Message",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send an Email1",
            "type": "main",
            "index": 0
          },
          {
            "node": "ERROR Message",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "RECOVERY Message",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "CRITICAL Message",
            "type": "main",
            "index": 0
          },
          {
            "node": "Count Critical Services",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "INFO Message": {
      "main": [
        [
          {
            "node": "Final Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "WARNING Message": {
      "main": [
        [
          {
            "node": "Final Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "ERROR Message": {
      "main": [
        [
          {
            "node": "Final Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "CRITICAL Message": {
      "main": [
        [
          {
            "node": "Final Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook1": {
      "main": [
        [
          {
            "node": "Format Detector",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Recent Logs": {
      "main": [
        [
          {
            "node": "Format Recent Logs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Recent Logs": {
      "main": [
        [
          {
            "node": "Build LLM Cache Key",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Alert Message": {
      "main": [
        [
          {
            "node": "Alert Email",
            "type": "main",
            "index": 0
          },
          {
            "node": "Reset ACK Time",
            "type": "main",
            "index": 0
          },
          {
            "node": "Twilio WhatsApp Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Count Critical Services": {
      "main": [
        [
          {
            "node": "System Wide Critical?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "System Wide Critical?": {
      "main": [
        [
          {
            "node": "Get All Critical Services",
            "type": "main",
            "index": 0
          },
          {
            "node": "Get Recent Logs",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Get Recent Logs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get All Critical Services": {
      "main": [
        [
          {
            "node": "Build Correlation Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Correlation Prompt": {
      "main": [
        [
          {
            "node": "Groq Correlation Call",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Groq Correlation Call": {
      "main": [
        [
          {
            "node": "Parse Correlation Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Correlation Response": {
      "main": [
        [
          {
            "node": "System Wide Alert Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
   

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

LogSentinel Workflow. Uses postgres, emailSend, httpRequest. Webhook trigger; 44 nodes.

Source: https://github.com/monosemantic/logsentinel/blob/1a156086fada97574471d20e1bdcf20f1ae3ca7c/workflows/log-ingestion.json — original creator credit. Request a take-down →

More Data & Sheets workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Data & Sheets

This n8n workflow automates the transformation of raw text ideas into structured visual diagrams and content assets using NapkinAI.

HTTP Request, Email Send, Postgres
Data & Sheets

Receive request via webhook with customer question Analyze sentiment and detect urgency using JavaScript Send urgent alerts to Slack for critical cases Search knowledge base and fetch conversation his

HTTP Request, Postgres, Email Send +1
Data & Sheets

Criador de Empresa Geral. Uses httpRequest, emailSend, postgres. Webhook trigger; 15 nodes.

HTTP Request, Email Send, Postgres
Data & Sheets

Send OTP Verification. Uses postgres, emailSend, httpRequest. Webhook trigger; 7 nodes.

Postgres, Email Send, HTTP Request
Data & Sheets

Send Invoice on Project Validated. Uses httpRequest, emailSend, postgres. Webhook trigger; 5 nodes.

HTTP Request, Email Send, Postgres