AutomationFlowsEmail & Gmail › Run a Weekly N8n Security Audit and Email Changes with Supabase and Gmail

Run a Weekly N8n Security Audit and Email Changes with Supabase and Gmail

ByFlowcheckers @jori on n8n.io

If your main Error Diagnosis workflow fails for any reason, this backup workflow sends an alert email to the workflow owner.

Cron / scheduled trigger★★★★☆ complexity15 nodesSupabaseGmailn8n
Email & Gmail Trigger: Cron / scheduled Nodes: 15 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #15884 — we link there as the canonical source.

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
{
  "id": "Cj80piKxAbqoPkK4",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Weekly Security Audit",
  "tags": [],
  "nodes": [
    {
      "id": "8ad17401-5292-49c9-9aca-7e2db5a6c7e6",
      "name": "Setup Instructions",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -144,
        -240
      ],
      "parameters": {
        "width": 480,
        "height": 704,
        "content": "## HOW IT WORKS\n\nEvery Monday at 9:00 AM, this workflow automatically runs a security audit on your n8n instance. The audit results are compared against the previous week's results stored in Supabase. If new or changed risks are detected, you receive an email via Gmail with a clear summary per risk category. If nothing has changed, the workflow stops silently. Either way, the latest audit result is always saved to Supabase for the next comparison.\n\n## SETUP INSTRUCTIONS\n\n**1. Credentials to configure:**\n- `n8n API` \u2014 Settings \u2192 API \u2192 Create key, add as n8n API credential\n- `Supabase` \u2014 Add your Supabase Project URL + service role API key\n- `Gmail OAuth2` \u2014 Connect Gmail via OAuth2\n\n**2. Create Supabase table** (run in SQL Editor):\n```sql\nCREATE TABLE n8n_audit_results (\n  id BIGSERIAL PRIMARY KEY,\n  created_at TIMESTAMPTZ DEFAULT NOW(),\n  audit_data JSONB NOT NULL\n);\n```\n\n**3. Gmail recipient:**\nIn the Gmail node, replace `recipient@example.com` with the actual address.\n\n**4. Activate** the workflow once all credentials are set.\n\nThe workflow runs every Monday at 09:00 and only sends an email when NEW or CHANGED security findings are detected compared to the previous week."
      },
      "typeVersion": 1
    },
    {
      "id": "452c9628-e342-4ad4-b5bf-8e60b811a2d4",
      "name": "Note Step 1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        384,
        192
      ],
      "parameters": {
        "color": 6,
        "width": 220,
        "height": 148,
        "content": "**Step 1 \u2014 Schedule Trigger**\nRuns every Monday at 09:00.\nChange the cron if needed."
      },
      "typeVersion": 1
    },
    {
      "id": "5ffb632f-47bf-4342-bbc2-5842eadaa02e",
      "name": "Note Step 2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        640,
        192
      ],
      "parameters": {
        "color": 6,
        "height": 148,
        "content": "**Step 2 \u2014 Generate Audit**\nCalls the built-in n8n Security Audit.\nChecks: Credentials, Database, File System, Nodes, Instance."
      },
      "typeVersion": 1
    },
    {
      "id": "addb5ee0-8bb0-44a8-b130-9826923d360e",
      "name": "Note Step 3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        912,
        192
      ],
      "parameters": {
        "color": 6,
        "height": 148,
        "content": "**Step 3 \u2014 Fetch Previous Result**\nRetrieves the most recent stored audit from Supabase to compare against."
      },
      "typeVersion": 1
    },
    {
      "id": "4f751dce-8fe5-4ddd-b3aa-c236e091b194",
      "name": "Note Step 4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1184,
        192
      ],
      "parameters": {
        "color": 6,
        "width": 260,
        "height": 142,
        "content": "**Step 4 \u2014 Compare Audits**\nCompares new vs previous audit across all 5 categories.\nOutputs: newFindings[], changedFindings[], hasChanges (boolean)."
      },
      "typeVersion": 1
    },
    {
      "id": "0f3425bd-286f-47db-ab01-e975f369f4d0",
      "name": "Note Step 5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1472,
        192
      ],
      "parameters": {
        "color": 6,
        "height": 142,
        "content": "**Step 5 \u2014 Store Result**\nAlways saves the new audit to Supabase, regardless of changes.\nEnsures next run has a baseline to compare against."
      },
      "typeVersion": 1
    },
    {
      "id": "91f3589b-005e-4e2b-b89a-bde04d9a5d36",
      "name": "Note Step 6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1472,
        512
      ],
      "parameters": {
        "color": 6,
        "height": 142,
        "content": "**Step 6 \u2014 Any New Findings?**\nOnly proceeds to email if new or changed risks were found."
      },
      "typeVersion": 1
    },
    {
      "id": "1962fca8-c47c-4438-858d-d1e72c334b23",
      "name": "Note Step 7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1744,
        512
      ],
      "parameters": {
        "color": 6,
        "height": 132,
        "content": "**Step 7 \u2014 Send Alert Email**\nSends a human-readable summary to the configured recipient.\nLists each risk category, finding count, and description."
      },
      "typeVersion": 1
    },
    {
      "id": "9adc66da-fa38-46a5-92f1-240f3ee6be20",
      "name": "Every Monday 09:00",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        432,
        352
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [
                1
              ],
              "triggerAtHour": 9
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "dc3797b5-ceff-49dc-82d4-9ae550d2d882",
      "name": "Fetch Previous Audit",
      "type": "n8n-nodes-base.supabase",
      "position": [
        976,
        352
      ],
      "parameters": {
        "limit": 1,
        "tableId": "n8n_audit_results",
        "operation": "getAll"
      },
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1,
      "alwaysOutputData": true
    },
    {
      "id": "cbc932e3-3075-438a-b8c8-e8d0ded7ecf8",
      "name": "Compare Audits",
      "type": "n8n-nodes-base.code",
      "position": [
        1248,
        352
      ],
      "parameters": {
        "jsCode": "// Retrieve the new audit result from the n8n Audit node\nconst newAudit = $('Generate a security audit').first().json;\n\n// Retrieve the previous audit from Supabase (may be empty on first run)\nconst previousItems = $('Fetch Previous Audit').all();\nlet previousAudit = null;\n\nif (previousItems.length > 0 && previousItems[0].json && previousItems[0].json.audit_data) {\n  const raw = previousItems[0].json.audit_data;\n  previousAudit = typeof raw === 'string' ? JSON.parse(raw) : raw;\n}\n\n// Map readable category labels to actual audit report keys\nconst categoryMap = {\n  'Credentials': 'Credentials Risk Report',\n  'Database': 'Database Risk Report',\n  'File System': 'Filesystem Risk Report',\n  'Nodes': 'Nodes Risk Report',\n  'Instance': 'Instance Risk Report'\n};\n\n// Compare new audit against previous audit per category\nconst newFindings = [];\nconst changedFindings = [];\n\nfor (const [label, reportKey] of Object.entries(categoryMap)) {\n  const newSections = (newAudit[reportKey] || {}).sections || [];\n  const prevSections = previousAudit ? ((previousAudit[reportKey] || {}).sections || []) : [];\n\n  for (const section of newSections) {\n    const prevSection = prevSections.find(s => s.title === section.title);\n\n    // Build affected list based on category and location type\n    let affected = [];\n    if (section.location) {\n      if (label === 'Nodes') {\n        // Group by workflow name with node count\n        const workflowCounts = {};\n        for (const l of section.location) {\n          const name = l.workflowName || l.nodeType || l.name || JSON.stringify(l);\n          workflowCounts[name] = (workflowCounts[name] || 0) + 1;\n        }\n        affected = Object.entries(workflowCounts).map(([name, count]) =>\n          count > 1 ? `${name} (${count} nodes)` : name\n        );\n      } else if (label === 'Instance') {\n        // Webhooks have a path, other instance items have a name\n        affected = section.location.map(l =>\n          l.webhookPath ? `${l.workflowName} \u2014 ${l.webhookPath}` :\n          l.name || l.nodeType || JSON.stringify(l)\n        );\n      } else {\n        affected = section.location.map(l =>\n          l.name || l.nodeType || l.packageUrl || JSON.stringify(l)\n        );\n      }\n    }\n\n    // Format version info dynamically from description and nextVersions\n    let extra = null;\n    if (section.nextVersions) {\n      extra = `${section.description}\\nAvailable updates: ${section.nextVersions.map(v => v.name).join(' / ')}`;\n    }\n\n    // Format security settings dynamically\n    if (section.settings) {\n      const s = section.settings;\n      const lines = [];\n      if (s.features) {\n        for (const [key, value] of Object.entries(s.features)) {\n          lines.push(`\u2022 ${key}: ${value ? 'enabled' : 'disabled'}`);\n        }\n      }\n      if (s.nodes) {\n        for (const [key, value] of Object.entries(s.nodes)) {\n          lines.push(`\u2022 ${key}: ${value || 'none'}`);\n        }\n      }\n      if (s.telemetry) {\n        for (const [key, value] of Object.entries(s.telemetry)) {\n          lines.push(`\u2022 ${key}: ${value ? 'enabled' : 'disabled'}`);\n        }\n      }\n      extra = lines.join('\\n');\n    }\n\n    const finding = {\n      category: label,\n      type: 'NEW',\n      title: section.title,\n      description: section.description,\n      recommendation: section.recommendation,\n      affected,\n      extra\n    };\n\n    if (!prevSection) {\n      newFindings.push(finding);\n    } else if (JSON.stringify(prevSection) !== JSON.stringify(section)) {\n      changedFindings.push({ ...finding, type: 'CHANGED' });\n    }\n  }\n}\n\n// Return findings and a boolean for the IF node\nreturn [{\n  json: {\n    newAudit,\n    newFindings,\n    changedFindings,\n    hasChanges: newFindings.length > 0 || changedFindings.length > 0\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "3370f5de-0a8b-4e66-a5db-0bf82b59ee3a",
      "name": "Store Audit Result",
      "type": "n8n-nodes-base.supabase",
      "position": [
        1536,
        352
      ],
      "parameters": {
        "tableId": "n8n_audit_results",
        "fieldsUi": {
          "fieldValues": [
            {
              "fieldId": "audit_data",
              "fieldValue": "={{ JSON.stringify($json.newAudit) }}"
            }
          ]
        }
      },
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "76b37108-2129-49ac-8851-0072abcab76e",
      "name": "Any New Findings?",
      "type": "n8n-nodes-base.if",
      "position": [
        1520,
        672
      ],
      "parameters": {
        "options": {
          "looseTypeValidation": true
        },
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-has-changes",
              "operator": {
                "type": "boolean",
                "operation": "true"
              },
              "leftValue": "={{ $('Compare Audits').item.json.hasChanges }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "bd4693e1-d2f5-4504-b079-6b1029f9f318",
      "name": "Send Alert Email",
      "type": "n8n-nodes-base.gmail",
      "position": [
        1808,
        656
      ],
      "parameters": {
        "sendTo": "user@example.com",
        "message": "=<p>The weekly security audit has detected <b>{{ $json.newFindings.length }} new</b> and <b>{{ $json.changedFindings.length }} changed</b> findings compared to last week.</p>\n\n<hr>\n\n{{ $json.newFindings.concat($json.changedFindings).map(f => `\n<p>\n  <b>[${f.type}] ${f.category} \u2014 ${f.title}</b><br><br>\n  ${f.description}<br><br>\n  ${f.affected && f.affected.length > 0 ? '<b>Affected:</b><br>' + f.affected.map(a => '\u2022 ' + a).join('<br>') + '<br><br>' : ''}\n  ${f.extra ? '<b>Details:</b><br>' + f.extra.replace(/\\n/g, '<br>') + '<br><br>' : ''}\n  <i>Recommendation: ${f.recommendation}</i>\n</p>\n<hr>\n`).join('') }}\n\n<p style=\"color:gray;font-size:12px\">Sent automatically by your n8n Weekly Security Audit workflow.</p>",
        "options": {},
        "subject": "n8n Security Audit - New findings detected"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "5d35455a-3ca7-499e-8f85-6ef6dcdf15fe",
      "name": "Generate a security audit",
      "type": "n8n-nodes-base.n8n",
      "position": [
        704,
        352
      ],
      "parameters": {
        "resource": "audit",
        "operation": "generate",
        "requestOptions": {},
        "additionalOptions": {}
      },
      "credentials": {
        "n8nApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "5fdec099-ce28-430a-b08d-f2518b8103c6",
  "connections": {
    "Compare Audits": {
      "main": [
        [
          {
            "node": "Store Audit Result",
            "type": "main",
            "index": 0
          },
          {
            "node": "Any New Findings?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Alert Email": {
      "main": [
        []
      ]
    },
    "Any New Findings?": {
      "main": [
        [
          {
            "node": "Send Alert Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Every Monday 09:00": {
      "main": [
        [
          {
            "node": "Generate a security audit",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Previous Audit": {
      "main": [
        [
          {
            "node": "Compare Audits",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate a security audit": {
      "main": [
        [
          {
            "node": "Fetch Previous Audit",
            "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

If your main Error Diagnosis workflow fails for any reason, this backup workflow sends an alert email to the workflow owner.

Source: https://n8n.io/workflows/15884/ — original creator credit. Request a take-down →

More Email & Gmail workflows → · Browse all categories →

Related workflows

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

Email & Gmail

This workflow gathers papers in Arxiv and specific arxiv category AI helps to make summarized form of newsletter and send it to subscriber using gmail Supabase Table schema user_email: Text - Mandator

Supabase, Gmail
Email & Gmail

Limit Code. Uses scheduleTrigger, n8n, googleDrive, splitInBatches. Scheduled trigger; 29 nodes.

n8n, Google Drive, Execution Data +3
Email & Gmail

Generate Weekly n8n Execution Report and Email Summary Automatically runs every 7 days to pull all n8n workflow executions from the past week Merges execution data with workflow information to provide

n8n, Gmail, Microsoft Outlook
Email & Gmail

uçak bileti. Uses httpRequest, telegram, gmail, supabase. Scheduled trigger; 10 nodes.

HTTP Request, Telegram, Gmail +1
Email & Gmail

YOUR_ID 4. Uses gmail, googleDrive, googleSheets, httpRequest. Scheduled trigger; 53 nodes.

Gmail, Google Drive, Google Sheets +1