This workflow corresponds to n8n.io template #7202 — we link there as the canonical source.
This workflow follows the Airtable → Gmail 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 →
{
"id": "bBRp9TmumfujcoHs",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Security Hub Alerts Triaged by AI",
"tags": [],
"nodes": [
{
"id": "7be0eeba-8700-4b51-a40f-db84c7c533b1",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"position": [
0,
-272
],
"parameters": {
"path": "aws-misconfig",
"options": {},
"httpMethod": "POST",
"responseMode": "lastNode"
},
"typeVersion": 2
},
{
"id": "ac0b1a8d-14a0-4e6d-be3a-779a57603869",
"name": "Normalize Finding",
"type": "n8n-nodes-base.code",
"position": [
672,
-176
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "// Code created by ca7ai\n// n8n Code node (Run Once for Each Item)\n// Normalizes Security Hub / AWS Config events whether they arrive at root or under body\nconst evt = $json.body ?? $json;\n\n// Security Hub finding (EventBridge or raw)\nconst sh = evt?.detail?.findings?.[0] || (Array.isArray(evt.findings) ? evt.findings[0] : null);\n// AWS Config notification\nconst cfg = evt?.detail?.configRuleName ? evt.detail : null;\n// Raw Security Hub finding at root\nconst rawSh = (!sh && evt?.ProductArn) ? evt : null;\n\nconst f = sh || rawSh || {};\n\nconst sev = f?.Severity?.Label || (cfg ? \"MEDIUM\" : \"UNKNOWN\");\nconst title = f?.Title || cfg?.configRuleName || \"Finding\";\nconst desc = f?.Description || cfg?.newEvaluationResult?.annotation || \"\u2014\";\nconst id = f?.Id\n || cfg?.newEvaluationResult?.evaluationResultIdentifier?.evaluationResultQualifier?.configRuleName\n || String(Date.now());\nconst res = f?.Resources?.[0]?.Id || cfg?.resourceId || \"unknown\";\nconst types = f?.Types || [];\nconst account = evt?.account || f?.AwsAccountId || \"unknown\";\nconst region = evt?.region || f?.Region || \"unknown\";\n\n// Derive service + hints\nlet service = \"UNKNOWN\";\nif (/^arn:aws:s3:::/.test(res) || types.some(t => t.includes(\"S3\"))) service = \"S3\";\nelse if (types.some(t => /SecurityGroup/i.test(t)) || /sg-/.test(res)) service = \"EC2-SG\";\nelse if (types.some(t => /IAM/i.test(t))) service = \"IAM\";\nelse if (types.some(t => /RDS|SQL|DB/i.test(t))) service = \"RDS\";\nelse if (f?.ProductArn) service = (f.ProductArn.split(\":\")[5] || \"UNKNOWN\");\n\nconst misconfig_hints = [];\nif (service === \"S3\") misconfig_hints.push(\"s3\");\nif (/0\\.0\\.0\\.0\\/0|Public|Open|world/i.test(desc)) misconfig_hints.push(\"public\");\nif (service === \"EC2-SG\") misconfig_hints.push(\"sg\");\nif (service === \"IAM\") misconfig_hints.push(\"iam\");\nif (service === \"RDS\") misconfig_hints.push(\"db\");\n\n// IMPORTANT: return a SINGLE object (not an array) in this mode\nreturn {\n finding_id: id,\n title,\n description: desc,\n severity: sev,\n resource_id: res,\n service,\n account,\n region,\n product_types: types,\n misconfig_hints,\n raw: evt,\n};"
},
"typeVersion": 2
},
{
"id": "be7fa618-9c8c-4418-a405-a4f2787faaf5",
"name": "Send a message",
"type": "n8n-nodes-base.gmail",
"position": [
1520,
-176
],
"parameters": {
"sendTo": "user@example.com",
"message": "=={{ (() => { const nf = $node[\"Normalize Finding\"].json; const ai = typeof $node[\"AI Prioritizer\"].json.message.content === 'string' ? JSON.parse($node[\"AI Prioritizer\"].json.message.content) : $node[\"AI Prioritizer\"].json.message.content; const steps = (ai.remediation || []).map(s => `<li>${s}</li>`).join(''); const tags = (ai.tags || []).join(', '); const airtableId = $node[\"Airtable - Create Record\"].json?.id || ''; const airtableLine = airtableId ? `<p><b>Airtable Record ID:</b> ${airtableId}</p>` : ''; return ` <h2>AWS Misconfig Alert</h2> <p><b>Priority:</b> ${ai.priority} <b>Severity:</b> ${nf.severity}</p> <p><b>Title:</b> ${nf.title}</p> <p><b>Service:</b> ${nf.service} <b>Resource:</b> ${nf.resource_id}</p> <p><b>Account:</b> ${nf.account} <b>Region:</b> ${nf.region}</p> <p><b>Why:</b> ${ai.rationale}</p> <p><b>Remediation:</b></p> <ol>${steps}</ol> <p><b>Tags:</b> ${tags || '\u2014'}</p> ${airtableLine} <details><summary>Raw finding</summary> <pre style=\"background:#f6f8fa;padding:12px;border-radius:6px;white-space:pre-wrap\">${JSON.stringify(nf.raw || nf, null, 2)}</pre> </details> `;})() }}",
"options": {},
"subject": "=={{ `[${JSON.parse($node[\"AI Prioritizer\"].json.message.content).priority}] ${$node[\"Normalize Finding\"].json.title} \u2014 ${$node[\"Normalize Finding\"].json.resource_id} (${ $node[\"Normalize Finding\"].json.service })` }}"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.1
},
{
"id": "c0fb42e3-e670-44f9-a3fa-0b2c3dec2ae1",
"name": "AI Prioritizer",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
896,
-176
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "gpt-4.1-mini",
"cachedResultName": "GPT-4.1-MINI"
},
"options": {},
"messages": {
"values": [
{
"content": "=You are a cloud SecOps triage assistant. Given a normalized AWS finding JSON, return a STRICT JSON object:\n\n{\n \"priority\": \"P0|P1|P2|P3\",\n \"rationale\": \"one-paragraph reason referencing severity, resource, and exposure\",\n \"remediation\": [\"step 1\", \"step 2\", \"...\"],\n \"tags\": [\"s3\",\"iam\",\"public\", \"...\"]\n}\n\nMapping guidance:\n- Treat publicly accessible data (e.g., public S3 buckets, 0.0.0.0/0 on admin ports, open RDS) as P0 or P1 depending on blast radius.\n- Internal-only or low impact \u2192 P2/P3.\n- If severity label is CRITICAL/HIGH, bias to P0/P1.\n\nFinding:\n{{ JSON.stringify($json, null, 2) }}\n"
}
]
},
"jsonOutput": true
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.8
},
{
"id": "42721c67-8c6d-4063-826b-b59e01a3d8ca",
"name": "Airtable - Create Record",
"type": "n8n-nodes-base.airtable",
"position": [
1296,
-176
],
"parameters": {
"base": {
"__rl": true,
"mode": "list",
"value": "appzIE2wRRYUbvl50",
"cachedResultUrl": "https://airtable.com/uuuu",
"cachedResultName": "misconfigs"
},
"table": {
"__rl": true,
"mode": "list",
"value": "tblPDewIrVYYYYNyi",
"cachedResultUrl": "https://airtable.com/uuuu/uuuu",
"cachedResultName": "finding_table"
},
"columns": {
"value": {
"id": "={{ $('Normalize Finding').item.json.finding_id }}",
"Tags": "={{ $json.message.content.tags[0] }}{{ $json.message.content.tags[1] }}{{ $json.message.content.tags[2] }}{{ $json.message.content.tags[3] }}{{ $json.message.content.tags[4] }}",
"Title": "={{ $('Normalize Finding').item.json.title }}",
"Region": "={{ $('Normalize Finding').item.json.region }}",
"Account": "={{ $('Normalize Finding').item.json.account }}",
"Service": "={{ $('Normalize Finding').item.json.service }}",
"Priority": "={{ $json.message.content.priority }}",
"Resource": "={{ $('Normalize Finding').item.json.raw.detail.findings[0].Resources[0].Id }}",
"Severity": "={{ $('Normalize Finding').item.json.severity }}",
"Rationale": "={{ $json.message.content.rationale }}",
"Finding ID": "={{ $('Normalize Finding').item.json.raw.detail.findings[0].Id }}",
"Remediation": "={{ $json.message.content.remediation[0] }}{{ $json.message.content.remediation[1] }}{{ $json.message.content.remediation[2] }}{{ $json.message.content.remediation[3] }}"
},
"schema": [
{
"id": "id",
"type": "string",
"display": true,
"removed": false,
"readOnly": true,
"required": false,
"displayName": "id",
"defaultMatch": true
},
{
"id": "Finding ID",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Finding ID",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Title",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Title",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Severity",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Severity",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Priority",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Priority",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Resource",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Resource",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Service",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Service",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Account",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Account",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Region",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Region",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Tags",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Tags",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Rationale",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Rationale",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Remediation",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Remediation",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"id"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "upsert"
},
"credentials": {
"airtableTokenApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.1
},
{
"id": "6f9bf9c6-61ee-4b25-844c-5f03c9979f9e",
"name": "Edit Fields",
"type": "n8n-nodes-base.set",
"position": [
1744,
-176
],
"parameters": {
"mode": "raw",
"options": {},
"jsonOutput": "={{\n {\n resp: {\n status: \"processed\",\n priority: $node[\"Airtable - Create Record\"].json.fields.Priority,\n finding_id: $node[\"Normalize Finding\"].json.finding_id\n }\n }\n}}\n"
},
"typeVersion": 3.4
},
{
"id": "fbb5d2af-7eb2-414f-b79e-2a51ddb9b21f",
"name": "SNS Handler",
"type": "n8n-nodes-base.code",
"position": [
224,
-272
],
"parameters": {
"jsCode": "const b = $json.body ?? $json;\nconst token = $json.query?.token ?? b.token;\nif (token !== 'MY_SUPER_TOKEN') throw new Error('unauthorized');\n\nif (b.Type === 'SubscriptionConfirmation' && b.SubscribeURL) {\n return { mode: 'confirm', subscribeUrl: b.SubscribeURL };\n}\n\nlet event = b;\nif (b.Type === 'Notification' && b.Message) {\n try { event = JSON.parse(b.Message); } catch {}\n}\nreturn { mode: 'notify', event };\n"
},
"typeVersion": 2
},
{
"id": "b61e5e70-697d-4fe4-9897-9a116aa5aff1",
"name": "If",
"type": "n8n-nodes-base.if",
"position": [
448,
-272
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "d192995f-8809-4b60-8f6a-b7bd2e3e47b0",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "=={{ $json.mode === 'confirm' }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.2
},
{
"id": "5c1145c7-6dd6-47e3-9fee-e1fb15318a22",
"name": "SNS Confirm",
"type": "n8n-nodes-base.httpRequest",
"position": [
672,
-368
],
"parameters": {
"url": "=={{ $json.subscribeUrl }}",
"options": {
"timeout": 15000,
"response": {
"response": {
"fullResponse": true,
"responseFormat": "json"
}
}
}
},
"typeVersion": 4.2
},
{
"id": "831507c3-b00f-44ec-8506-c0310904eb6e",
"name": "Edit Fields1",
"type": "n8n-nodes-base.set",
"position": [
984,
-368
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "346708e7-629e-4d8a-8a98-994b9526b55d",
"name": "resp",
"type": "object",
"value": "=resp = { status: \"subscribed\", statusCode: $json.statusCode || 200 }"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "b39a925c-911d-4e61-a0e6-5aadc4ccfd38",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-464,
-32
],
"parameters": {
"width": 336,
"content": "## Note\n\n: You must have the AWS Side pre-configured before testing / starting this workflow\n"
},
"typeVersion": 1
},
{
"id": "1bea05cd-6468-4c74-aeec-8c60d69411c4",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-464,
-368
],
"parameters": {
"color": 4,
"width": 336,
"height": 304,
"content": "## Title: How it works (wiring)\n\n- **Flow**: Webhook \u2192 SNS Handler \u2192 IF \u2192 (true) SNS Confirm \u2192 Done | (false) Normalize \u2192 AI \u2192 Airtable \u2192 Gmail \u2192 Respond\n \n- **Purpose**: triage AWS misconfigs and alert the team\n \n- Responds when last node finishes (returns small JSON)\n"
},
"typeVersion": 1
}
],
"active": true,
"settings": {
"executionOrder": "v1"
},
"versionId": "044dc52b-81c7-43ab-a1c2-5be287a0d970",
"connections": {
"If": {
"main": [
[
{
"node": "SNS Confirm",
"type": "main",
"index": 0
}
],
[
{
"node": "Normalize Finding",
"type": "main",
"index": 0
}
]
]
},
"Webhook": {
"main": [
[
{
"node": "SNS Handler",
"type": "main",
"index": 0
}
]
]
},
"SNS Confirm": {
"main": [
[
{
"node": "Edit Fields1",
"type": "main",
"index": 0
}
]
]
},
"SNS Handler": {
"main": [
[
{
"node": "If",
"type": "main",
"index": 0
}
]
]
},
"Edit Fields1": {
"main": [
[]
]
},
"AI Prioritizer": {
"main": [
[
{
"node": "Airtable - Create Record",
"type": "main",
"index": 0
}
]
]
},
"Send a message": {
"main": [
[
{
"node": "Edit Fields",
"type": "main",
"index": 0
}
]
]
},
"Normalize Finding": {
"main": [
[
{
"node": "AI Prioritizer",
"type": "main",
"index": 0
}
]
]
},
"Airtable - Create Record": {
"main": [
[
{
"node": "Send a 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.
airtableTokenApigmailOAuth2openAiApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Automatically triages risky AWS misconfigurations and alerts your team.
Source: https://n8n.io/workflows/7202/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
This powerful n8n automation workflow is designed to execute advanced B2B lead enrichment and hyper-personalization for cold email outreach. By orchestrating a complex chain of data scraping, AI analy
User Signup & Verification: The workflow starts when a user signs up. It generates a verification code and sends it via SMS using Twilio. Code Validation: The user replies with the code. The workflow
This template is perfect for e-commerce entrepreneurs, marketers, agencies, and creative teams who want to turn simple product photos and short descriptions into professional flyers or product videos—
Instantly map all internal URLs, perform AI-powered (ChatGPT) analysis, and deliver results in HTML via webhook, Google Sheets, or email. All from your own n8n instance!
Watch on Youtube▶️