This workflow corresponds to n8n.io template #15207 — we link there as the canonical source.
This workflow follows the Gmail → Gmail Trigger 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": "uA8EiLrfPrRgVqBu",
"name": "Scan Incoming Email Attachments for Threats with VirusTotal and AI",
"tags": [],
"nodes": [
{
"id": "edd08ff9-d57a-4b27-9dba-b08b51ce1802",
"name": "README",
"type": "n8n-nodes-base.stickyNote",
"position": [
-160,
-320
],
"parameters": {
"width": 552,
"height": 776,
"content": "# Scan Incoming Email Attachments for Threats with VirusTotal and AI\n\nEvery minute, the workflow polls Gmail for unread emails with attachments, hashes each file for VirusTotal lookup, and asks AI whether the email text looks like phishing. A rule-based scorer combines both signals and routes the attachment to one of three outcomes: quarantine + Slack alert, human review queue, or safe Drive folder.\n\n## How it works\n1. Detect a new email with an attachment\n2. Hash each attachment with SHA256\n3. Look up the hash in VirusTotal\n4. Run an AI phishing check on the email text\n5. Combine both signals into a risk level (danger / suspicious / safe)\n6. Route to the matching action branch\n\n## Setup steps\n1. Get a free VirusTotal API key and add it as a Header Auth credential (Name: x-apikey)\n2. Create a Gmail label called QUARANTINE and a Google Drive folder for safe files\n3. Create a Google Sheet with columns: timestamp, email_from, email_subject, attachment, malicious_count, ai_verdict\n4. Open Set Configuration and fill in the Sheet ID, Drive folder ID, Slack channel, and label name\n5. Connect Gmail, Google Sheets, Google Drive, Slack, OpenAI, and VirusTotal credentials\n6. Activate the workflow"
},
"typeVersion": 1
},
{
"id": "73d67af0-5a99-4f48-befd-dc2dcd3a3c38",
"name": "Section 1 Trigger & Configure",
"type": "n8n-nodes-base.stickyNote",
"position": [
416,
-320
],
"parameters": {
"color": 7,
"width": 420,
"height": 340,
"content": "## 1. Trigger & Configure\nPoll Gmail every minute for unread messages with attachments, then load all editable values (Sheet ID, Drive folder ID, Slack channel, quarantine label) from a single Set Configuration node."
},
"typeVersion": 1
},
{
"id": "f80d9ede-cbf4-45b6-ae5f-a8307a0d4d0a",
"name": "Section 2 Scan",
"type": "n8n-nodes-base.stickyNote",
"position": [
864,
-320
],
"parameters": {
"color": 7,
"width": 700,
"height": 340,
"content": "## 2. Scan\nSplit the email into one item per attachment, calculate a SHA256 hash, look it up in VirusTotal, and run an AI phishing classifier on the subject and body."
},
"typeVersion": 1
},
{
"id": "d26a61a1-8546-4c4d-8c8a-cf082bf0f3b8",
"name": "Section 3 Score & Route",
"type": "n8n-nodes-base.stickyNote",
"position": [
1584,
-320
],
"parameters": {
"color": 7,
"width": 580,
"height": 340,
"content": "## 3. Score & Route\nCombine the VirusTotal malicious count and the AI verdict into a single risk level. AI never decides quarantine alone \u2014 the final rule is deterministic."
},
"typeVersion": 1
},
{
"id": "4df7944f-ec3c-45d0-9d52-354244d9385d",
"name": "Section 4A Danger",
"type": "n8n-nodes-base.stickyNote",
"position": [
2176,
-320
],
"parameters": {
"color": 7,
"width": 484,
"height": 312,
"content": "## 4A. Danger \u2192 Quarantine\nHigh-risk attachments: apply the QUARANTINE Gmail label and push a Slack alert to the security channel."
},
"typeVersion": 1
},
{
"id": "0a1d3462-f5dd-447d-bbb3-01c7775f3596",
"name": "Section 4B Suspicious",
"type": "n8n-nodes-base.stickyNote",
"position": [
2176,
16
],
"parameters": {
"color": 7,
"width": 340,
"height": 312,
"content": "## 4B. Suspicious \u2192 Human Review\nLog suspicious attachments to a Google Sheet review queue so a human can make the final call."
},
"typeVersion": 1
},
{
"id": "d3996539-6309-4674-97d0-6031e619418a",
"name": "Section 4C Safe",
"type": "n8n-nodes-base.stickyNote",
"position": [
2176,
352
],
"parameters": {
"color": 7,
"width": 340,
"height": 296,
"content": "## 4C. Safe \u2192 Drive\nSave clean attachments to the designated safe Google Drive folder for normal use."
},
"typeVersion": 1
},
{
"id": "372d6818-7f5d-42b2-bca6-8157403dc8ca",
"name": "Poll Gmail for New Attachments",
"type": "n8n-nodes-base.gmailTrigger",
"position": [
464,
-160
],
"parameters": {
"filters": {
"q": "has:attachment is:unread"
},
"pollTimes": {
"item": [
{
"mode": "everyMinute"
}
]
}
},
"typeVersion": 1.2
},
{
"id": "60ba2bf3-3913-4cff-aa20-109bbffda38f",
"name": "Set Configuration",
"type": "n8n-nodes-base.set",
"position": [
688,
-160
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "cfg-1",
"name": "review_sheet_id",
"type": "string",
"value": "REPLACE_WITH_YOUR_SHEET_ID"
},
{
"id": "cfg-2",
"name": "review_sheet_name",
"type": "string",
"value": "suspicious_queue"
},
{
"id": "cfg-3",
"name": "security_slack_channel",
"type": "string",
"value": "REPLACE_WITH_SLACK_CHANNEL"
},
{
"id": "cfg-4",
"name": "quarantine_label",
"type": "string",
"value": "REPLACE_WITH_QUARANTINE_LABEL"
},
{
"id": "cfg-5",
"name": "safe_drive_folder_id",
"type": "string",
"value": "REPLACE_WITH_DRIVE_FOLDER_ID"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "86e1a004-7b68-4e40-89dc-f14f284256d8",
"name": "Extract Attachments from Email",
"type": "n8n-nodes-base.code",
"position": [
896,
-160
],
"parameters": {
"jsCode": "// Split the email into one item per attachment\nconst items = $input.all();\nconst output = [];\n\nfor (const item of items) {\n const data = item.json;\n const binary = item.binary || {};\n\n for (const [key, attachment] of Object.entries(binary)) {\n output.push({\n json: {\n email_id: data.id,\n email_subject: data.subject || '',\n email_from: data.from?.value?.[0]?.address || data.from || '',\n email_body: (data.textPlain || data.snippet || '').substring(0, 2000),\n attachment_filename: attachment.fileName || 'unknown',\n attachment_mime: attachment.mimeType || '',\n attachment_size: attachment.fileSize || 0\n },\n binary: {\n attachment: attachment\n }\n });\n }\n}\n\nreturn output;"
},
"typeVersion": 2
},
{
"id": "bf9f6a52-40aa-4ebe-8b3e-ce921cb5679c",
"name": "Compute SHA256 Hash of Attachment",
"type": "n8n-nodes-base.code",
"position": [
1072,
-160
],
"parameters": {
"jsCode": "// Calculate SHA256 hash for VirusTotal lookup\nconst crypto = require('crypto');\nconst output = [];\n\nfor (const item of $input.all()) {\n const binaryData = item.binary.attachment;\n const buffer = Buffer.from(binaryData.data, 'base64');\n const hash = crypto.createHash('sha256').update(buffer).digest('hex');\n\n output.push({\n json: {\n ...item.json,\n file_hash: hash\n },\n binary: item.binary\n });\n}\n\nreturn output;"
},
"typeVersion": 2
},
{
"id": "f4d22ceb-7341-4d27-8304-0cb6f6c6b950",
"name": "Look Up Hash in VirusTotal",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"maxTries": 3,
"position": [
1424,
-160
],
"parameters": {
"url": "=https://www.virustotal.com/api/v3/files/{{ $json.file_hash }}",
"options": {},
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth"
},
"retryOnFail": true,
"typeVersion": 4.2,
"continueOnFail": true,
"waitBetweenTries": 2000
},
{
"id": "2ba6af90-373e-4480-91f9-97ef14ce98f5",
"name": "Classify Phishing Risk with OpenAI",
"type": "@n8n/n8n-nodes-langchain.openAi",
"onError": "continueRegularOutput",
"maxTries": 2,
"position": [
1600,
-160
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "gpt-4o-mini",
"cachedResultName": "gpt-4o-mini"
},
"options": {},
"messages": {
"values": [
{
"content": "You are an email phishing detection assistant. Analyze the email below and return a JSON object with two fields: \"verdict\" (one of: \"phishing\", \"suspicious\", \"safe\") and \"reason\" (short explanation, max 200 chars).\n\nEmail From: {{ $json.email_from }}\nEmail Subject: {{ $json.email_subject }}\nEmail Body:\n{{ $json.email_body }}\n\nRespond ONLY with valid JSON. No markdown, no prose."
}
]
},
"jsonOutput": true
},
"retryOnFail": true,
"typeVersion": 1.8,
"waitBetweenTries": 2000
},
{
"id": "ab3db115-f4d0-42fb-83c2-6c3050e526f1",
"name": "Calculate Combined Risk Level",
"type": "n8n-nodes-base.code",
"position": [
1872,
-160
],
"parameters": {
"jsCode": "// Combine VirusTotal + AI verdict into a single risk level\nconst item = $input.first();\nconst data = item.json;\n\nconst hashData = $('Compute SHA256 Hash of Attachment').item.json;\n\nlet maliciousCount = 0;\nlet suspiciousCount = 0;\nlet vtAvailable = false;\n\ntry {\n const vtData = $('Look Up Hash in VirusTotal').item.json;\n if (vtData && vtData.data && vtData.data.attributes) {\n const stats = vtData.data.attributes.last_analysis_stats || {};\n maliciousCount = stats.malicious || 0;\n suspiciousCount = stats.suspicious || 0;\n vtAvailable = true;\n }\n} catch (e) {\n // File unknown to VirusTotal - treat as not-yet-seen\n}\n\nconst aiVerdict = (data.message?.content || '').toLowerCase().trim();\n\nlet risk_level;\nif (maliciousCount >= 3 || aiVerdict.includes('phishing')) {\n risk_level = 'danger';\n} else if (maliciousCount > 0 || suspiciousCount >= 2 || aiVerdict.includes('suspicious')) {\n risk_level = 'suspicious';\n} else {\n risk_level = 'safe';\n}\n\nreturn {\n json: {\n ...hashData,\n malicious_count: maliciousCount,\n suspicious_count: suspiciousCount,\n vt_available: vtAvailable,\n ai_verdict: aiVerdict,\n risk_level\n },\n binary: item.binary\n};"
},
"typeVersion": 2
},
{
"id": "4a97061c-49a3-418a-9c98-2801ca972c8a",
"name": "Route by Threat Level",
"type": "n8n-nodes-base.switch",
"position": [
2032,
-176
],
"parameters": {
"rules": {
"values": [
{
"outputKey": "danger",
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "sw-1",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.risk_level }}",
"rightValue": "danger"
}
]
},
"renameOutput": true
},
{
"outputKey": "suspicious",
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "sw-2",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.risk_level }}",
"rightValue": "suspicious"
}
]
},
"renameOutput": true
},
{
"outputKey": "safe",
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "sw-3",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.risk_level }}",
"rightValue": "safe"
}
]
},
"renameOutput": true
}
]
},
"options": {}
},
"typeVersion": 3.2
},
{
"id": "77a67f98-f9c3-4f4a-93e6-4be8f7a90649",
"name": "Apply Quarantine Label on Gmail",
"type": "n8n-nodes-base.gmail",
"onError": "continueRegularOutput",
"maxTries": 3,
"position": [
2272,
-192
],
"parameters": {
"labelIds": "={{ [$('Set Configuration').item.json.quarantine_label] }}",
"messageId": "={{ $json.email_id }}",
"operation": "addLabels"
},
"retryOnFail": true,
"typeVersion": 2.1,
"waitBetweenTries": 2000
},
{
"id": "c7627934-d71b-4c92-b1a4-01ca0eb1db12",
"name": "Send Security Alert to Slack",
"type": "n8n-nodes-base.slack",
"onError": "continueRegularOutput",
"maxTries": 3,
"position": [
2480,
-192
],
"parameters": {
"text": "=DANGEROUS email attachment detected\n\n- From: {{ $json.email_from }}\n- Subject: {{ $json.email_subject }}\n- File: {{ $json.attachment_filename }}\n- VirusTotal malicious count: {{ $json.malicious_count }}\n- AI verdict: {{ $json.ai_verdict }}\n\nEmail was quarantined automatically.",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "name",
"value": "={{ $('Set Configuration').item.json.security_slack_channel }}"
},
"otherOptions": {}
},
"retryOnFail": true,
"typeVersion": 2.2,
"waitBetweenTries": 2000
},
{
"id": "697a9e3b-ffa8-4de7-a960-80166efbe457",
"name": "Log to Review Queue on Google Sheets",
"type": "n8n-nodes-base.googleSheets",
"onError": "continueRegularOutput",
"maxTries": 3,
"position": [
2272,
160
],
"parameters": {
"columns": {
"value": {
"timestamp": "={{ $now.toISO() }}",
"ai_verdict": "={{ $json.ai_verdict }}",
"attachment": "={{ $json.attachment_filename }}",
"email_from": "={{ $json.email_from }}",
"email_subject": "={{ $json.email_subject }}",
"malicious_count": "={{ $json.malicious_count }}"
},
"mappingMode": "defineBelow",
"matchingColumns": []
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "name",
"value": "={{ $('Set Configuration').item.json.review_sheet_name }}"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "={{ $('Set Configuration').item.json.review_sheet_id }}"
}
},
"retryOnFail": true,
"typeVersion": 4.5,
"waitBetweenTries": 2000
},
{
"id": "888f4fc8-4962-4282-8d9a-c8f654cfaf6e",
"name": "Save Attachment to Safe Drive Folder",
"type": "n8n-nodes-base.googleDrive",
"onError": "continueRegularOutput",
"maxTries": 3,
"position": [
2272,
480
],
"parameters": {
"name": "={{ $json.attachment_filename }}",
"driveId": {
"__rl": true,
"mode": "list",
"value": "My Drive"
},
"options": {},
"folderId": {
"__rl": true,
"mode": "id",
"value": "={{ $('Set Configuration').item.json.safe_drive_folder_id }}"
}
},
"retryOnFail": true,
"typeVersion": 3,
"waitBetweenTries": 2000
},
{
"id": "wait-9e8177ac",
"name": "Wait 15s for VirusTotal Rate Limit",
"type": "n8n-nodes-base.wait",
"position": [
1248,
-160
],
"parameters": {
"amount": 15
},
"typeVersion": 1.1
}
],
"active": false,
"settings": {
"binaryMode": "separate",
"executionOrder": "v1"
},
"versionId": "f35f9153-01ce-4cda-afb6-5c53f4e3f945",
"connections": {
"Set Configuration": {
"main": [
[
{
"node": "Extract Attachments from Email",
"type": "main",
"index": 0
}
]
]
},
"Route by Threat Level": {
"main": [
[
{
"node": "Apply Quarantine Label on Gmail",
"type": "main",
"index": 0
}
],
[
{
"node": "Log to Review Queue on Google Sheets",
"type": "main",
"index": 0
}
],
[
{
"node": "Save Attachment to Safe Drive Folder",
"type": "main",
"index": 0
}
]
]
},
"Look Up Hash in VirusTotal": {
"main": [
[
{
"node": "Classify Phishing Risk with OpenAI",
"type": "main",
"index": 0
}
]
]
},
"Calculate Combined Risk Level": {
"main": [
[
{
"node": "Route by Threat Level",
"type": "main",
"index": 0
}
]
]
},
"Extract Attachments from Email": {
"main": [
[
{
"node": "Compute SHA256 Hash of Attachment",
"type": "main",
"index": 0
}
]
]
},
"Poll Gmail for New Attachments": {
"main": [
[
{
"node": "Set Configuration",
"type": "main",
"index": 0
}
]
]
},
"Apply Quarantine Label on Gmail": {
"main": [
[
{
"node": "Send Security Alert to Slack",
"type": "main",
"index": 0
}
]
]
},
"Compute SHA256 Hash of Attachment": {
"main": [
[
{
"node": "Wait 15s for VirusTotal Rate Limit",
"type": "main",
"index": 0
}
]
]
},
"Classify Phishing Risk with OpenAI": {
"main": [
[
{
"node": "Calculate Combined Risk Level",
"type": "main",
"index": 0
}
]
]
},
"Wait 15s for VirusTotal Rate Limit": {
"main": [
[
{
"node": "Look Up Hash in VirusTotal",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Small teams, solo operators, and security-conscious individuals who receive email attachments from external senders. Useful for freelancers, agencies, HR teams, and anyone handling CVs, invoices, or documents from unknown sources.
Source: https://n8n.io/workflows/15207/ — 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.
Overview
This workflow converts emailed timesheets into structured invoice rows in Google Sheets and stores them in the correct Google Drive folder structure.
Complete AI-powered sales system Automates lead capture, qualification, and follow-up from multiple channels. AI INTELLIGENCE:
This n8n workflow — HRMate — streamlines your entire recruitment process by automatically parsing incoming job applications, evaluating candidate fit using AI, and sending personalized acceptance or r
💥 Automate YouTube thumbnail creation from video links -vide. Uses telegramTrigger, httpRequest, googleDrive, gmail. Event-driven trigger; 25 nodes.