Track commodity portfolio drift with Google Sheets, Gemini AI and Gmail alerts
Pull contacts, verify each address with BillionVerify, and continue to Drift — only deliverable addresses get through.
Why verify before the send
Sending to invalid, risky, catch-all, or disposable addresses spikes your bounce rate and erodes sender reputation. A verification gate before the Drift step removes that risk automatically — only deliverable addresses continue, the rest are flagged.
The workflow
BillionVerify — verification sits right before the send.
Node by node
- 1Daily Portfolio Rebalance CheckTrigger· n8n
Starts the workflow — on a schedule, a webhook, or manually while you test.
- 2Read HoldingsSource· n8n
Provides or transforms the contact data flowing through the workflow.
- 3Workflow SettingsSource· n8n
Provides or transforms the contact data flowing through the workflow.
- 4Synchronize InputsSource· n8n
Provides or transforms the contact data flowing through the workflow.
- 5Prepare Portfolio ContextSource· n8n
Provides or transforms the contact data flowing through the workflow.
- 6Validate Portfolio DataSource· n8n
Provides or transforms the contact data flowing through the workflow.
- 7Check Validation StatusLogic· n8n
Branches on the verification result: only deliverable addresses continue to the send; the rest are skipped and flagged.
- 8Calculate Portfolio AllocationSource· n8n
Provides or transforms the contact data flowing through the workflow.
- 9Prepare Validation Failure LogSource· n8n
Provides or transforms the contact data flowing through the workflow.
- 10Detect Portfolio DriftSource· n8n
Provides or transforms the contact data flowing through the workflow.
- 11Log Validation FailureSource· n8n
Provides or transforms the contact data flowing through the workflow.
- 12Check Rebalance RequirementLogic· n8n
Branches on the verification result: only deliverable addresses continue to the send; the rest are skipped and flagged.
- 13Classify Alert SeveritySource· n8n
Provides or transforms the contact data flowing through the workflow.
- 14Prepare No Action LogSource· n8n
Provides or transforms the contact data flowing through the workflow.
- 15Build Alert PromptSource· n8n
Provides or transforms the contact data flowing through the workflow.
- 16Log No ActionSource· n8n
Provides or transforms the contact data flowing through the workflow.
- 17Generate Alert MessageSource· n8n
Provides or transforms the contact data flowing through the workflow.
- 18Merge Alert DataSource· n8n
Provides or transforms the contact data flowing through the workflow.
- 19Prepare Alert PayloadSource· n8n
Provides or transforms the contact data flowing through the workflow.
- 20Log Sent AlertSource· n8n
Provides or transforms the contact data flowing through the workflow.
- 21Verify Email (BillionVerify)Verify· billionverify
The BillionVerify node verifies the address — status (valid / invalid / risky / catch-all / role / disposable), is_deliverable, and a confidence score — before anything is sent.
- 22IF deliverableLogic· n8n
Branches on the verification result: only deliverable addresses continue to the send; the rest are skipped and flagged.
- 23Send Rebalance EmailSend· n8n
Sends only to verified, deliverable addresses. Swap in your own provider node if you send elsewhere.
Workflow JSON
Copy or download this workflow, then import it in n8n (Workflows → Import from File / Paste). Install the BillionVerify community node first, then add your API key credential.
{
"name": "Track commodity portfolio drift with Google Sheets, Gemini AI and Gmail alerts + BillionVerify",
"nodes": [
{
"id": "7292b81c-f3fb-4b68-abf6-f3a66e703167",
"name": "Overview Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
3424,
3728
],
"parameters": {
"width": 1102,
"height": 316,
"content": "## Commodity Portfolio Tracker\n\n**How it works** \nThis workflow runs on a schedule, reads your holdings from Google Sheets, loads target allocation rules from the workflow config node, validates the data, calculates actual allocation, detects drift against your allowed range, classifies the alert severity, generates a plain-text rebalance message with Gemini, emails the result and logs the workflow outcome.\n\n**Setup steps** \n1. Connect Google Sheets, Gmail and Gemini credentials. \n2. Confirm the holdings sheet and spreadsheet are correct. \n3. Create a `rebalance_log` sheet with log columns. \n4. Update target allocation, alert email and thresholds in the config node. \n5. Test the workflow once manually, then activate the schedule."
},
"typeVersion": 1
},
{
"id": "3a83644b-da4f-4f5c-b6be-d51da74b4af3",
"name": "Input and Settings Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
3424,
4112
],
"parameters": {
"color": 7,
"width": 932,
"height": 424,
"content": "## Input and Settings\n\nThis section pulls the latest holdings from Google Sheets and loads the portfolio rules used in the workflow. The settings node acts like the control panel, where you can update target allocation, allowed ranges, alert email, currency and severity rules without changing the rest of the flow."
},
"typeVersion": 1
},
{
"id": "233431cb-acd0-41bb-8fe6-9410238a2bf6",
"name": "Analysis and Decision Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
4416,
4096
],
"parameters": {
"color": 7,
"width": 1076,
"height": 584,
"content": "## Analysis and Decision\n\nThis part checks whether the input data is clean and usable before doing any calculations. It then works out the actual allocation for each asset, compares it with the target range, identifies which assets are overweight or underweight and decides whether a rebalance alert is needed."
},
"typeVersion": 1
},
{
"id": "8527d92a-918b-4e5c-af4f-86b0f81508c6",
"name": "Alert Message and Logging Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
5552,
3904
],
"parameters": {
"color": 7,
"width": 1304,
"height": 616,
"content": "## Alert Message and Logging\n\nOnce drift is found, this section turns the result into a clear alert message. Gemini helps format the recommendation in simple language, the email node sends it to the selected recipient and the final step stores the outcome in the log sheet so you have a record of what happened in each run."
},
"typeVersion": 1
},
{
"id": "7dd2c230-3740-4a49-a914-026636a254eb",
"name": "Read Holdings",
"type": "n8n-nodes-base.googleSheets",
"position": [
3760,
4384
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1_lcrJ7b0c9ZqyenXZZZ-A5Yyb99lS-k8qSs-Y9PV3EA/edit#gid=0",
"cachedResultName": "holdings"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1_lcrJ7b0c9ZqyenXZZZ-A5Yyb99lS-k8qSs-Y9PV3EA",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1_lcrJ7b0c9ZqyenXZZZ-A5Yyb99lS-k8qSs-Y9PV3EA/edit?usp=drivesdk",
"cachedResultName": "Commodity Portfolio"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"id": "credential-id",
"name": "Google Sheets account"
}
},
"typeVersion": 4.7
},
{
"id": "79ce3c3c-442a-4a68-af7d-1a7eac2e6039",
"name": "Workflow Settings",
"type": "n8n-nodes-base.set",
"position": [
3760,
4224
],
"parameters": {
"mode": "raw",
"options": {},
"jsonOutput": "{\n \"targets\": [\n { \"asset\": \"Gold\", \"target_pct\": 40, \"min_pct\": 35, \"max_pct\": 45 },\n { \"asset\": \"Silver\", \"target_pct\": 20, \"min_pct\": 15, \"max_pct\": 25 },\n { \"asset\": \"Oil\", \"target_pct\": 20, \"min_pct\": 15, \"max_pct\": 25 },\n { \"asset\": \"Copper\", \"target_pct\": 10, \"min_pct\": 5, \"max_pct\": 15 },\n { \"asset\": \"Natural Gas\", \"target_pct\": 10, \"min_pct\": 5, \"max_pct\": 15 }\n ],\n \"threshold_pct\": 5,\n \"cooldown_days\": 3,\n \"alert_email\": \"user@example.com\",\n \"email_enabled\": \"yes\",\n \"ai_enabled\": \"yes\",\n \"portfolio_name\": \"Commodity Portfolio\",\n \"currency\": \"INR\",\n \"rebalance_mode\": \"amount\",\n \"severity_rules\": {\n \"low_max\": 5,\n \"medium_max\": 10\n }\n}"
},
"typeVersion": 3.4
},
{
"id": "7d538dfb-8097-4512-b38a-80bd479d671a",
"name": "Synchronize Inputs",
"type": "n8n-nodes-base.merge",
"position": [
4048,
4352
],
"parameters": {},
"typeVersion": 3.2
},
{
"id": "c0746678-d2e6-4459-8365-9c20c86a7373",
"name": "Prepare Portfolio Context",
"type": "n8n-nodes-base.code",
"position": [
4240,
4352
],
"parameters": {
"jsCode": "const holdings = $('Read Holdings').all().map(i => i.json);\nconst config = $('Workflow Settings').first().json;\n\nreturn [\n {\n json: {\n holdings,\n config\n }\n }\n];"
},
"typeVersion": 2
},
{
"id": "91c40090-c446-45d2-ac33-f9e4c3bec82c",
"name": "Validate Portfolio Data",
"type": "n8n-nodes-base.code",
"position": [
4464,
4352
],
"parameters": {
"jsCode": "const data = $input.first().json;\nconst holdings = data.holdings || [];\nconst config = data.config || {};\nconst targets = config.targets || [];\n\nif (!holdings.length) {\n return [{\n json: {\n valid: false,\n error: 'No holdings found.',\n holdings,\n config\n }\n }];\n}\n\nif (!targets.length) {\n return [{\n json: {\n valid: false,\n error: 'Targets are missing in workflow settings.',\n holdings,\n config\n }\n }];\n}\n\nconst targetTotal = targets.reduce((sum, t) => sum + Number(t.target_pct || 0), 0);\nif (targetTotal !== 100) {\n return [{\n json: {\n valid: false,\n error: `Target allocation must total 100. Current total is ${targetTotal}.`,\n holdings,\n config\n }\n }];\n}\n\nconst seenAssets = new Set();\n\nfor (const h of holdings) {\n const asset = String(h.asset || '').trim();\n const units = Number(h.units);\n const price = Number(h.price);\n\n if (!asset) {\n return [{\n json: {\n valid: false,\n error: 'One holding has a blank asset name.',\n holdings,\n config\n }\n }];\n }\n\n if (seenAssets.has(asset)) {\n return [{\n json: {\n valid: false,\n error: `Duplicate asset found in holdings: ${asset}`,\n holdings,\n config\n }\n }];\n }\n seenAssets.add(asset);\n\n if (Number.isNaN(units) || units < 0) {\n return [{\n json: {\n valid: false,\n error: `Invalid units for asset: ${asset}`,\n holdings,\n config\n }\n }];\n }\n\n if (Number.isNaN(price) || price < 0) {\n return [{\n json: {\n valid: false,\n error: `Invalid price for asset: ${asset}`,\n holdings,\n config\n }\n }];\n }\n\n const target = targets.find(t => String(t.asset || '').trim() === asset);\n if (!target) {\n return [{\n json: {\n valid: false,\n error: `No target config found for asset: ${asset}`,\n holdings,\n config\n }\n }];\n }\n}\n\nreturn [{\n json: {\n valid: true,\n error: null,\n holdings,\n config\n }\n}];"
},
"typeVersion": 2
},
{
"id": "f66778ac-7cb4-4ce7-b4cc-81e08796c440",
"name": "Check Validation Status",
"type": "n8n-nodes-base.if",
"position": [
4704,
4352
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.valid }}",
"rightValue": true
}
]
}
},
"typeVersion": 2.3
},
{
"id": "52d2ef3f-d231-49b2-b7b2-e134326fe6a4",
"name": "Calculate Portfolio Allocation",
"type": "n8n-nodes-base.code",
"position": [
4928,
4240
],
"parameters": {
"jsCode": "const data = $input.first().json;\nconst holdings = data.holdings;\nconst config = data.config;\n\nconst enrichedHoldings = holdings.map(h => {\n const units = Number(h.units);\n const price = Number(h.price);\n const current_value = Number(h.current_value ?? (units * price));\n\n return {\n row_number: h.row_number,\n asset: h.asset,\n units,\n price,\n current_value,\n last_updated: h.last_updated\n };\n});\n\nconst total_value = enrichedHoldings.reduce((sum, h) => sum + h.current_value, 0);\n\nconst assets = enrichedHoldings.map(h => {\n const target = config.targets.find(t => t.asset === h.asset);\n\n if (!target) {\n throw new Error(`Missing target config for asset: ${h.asset}`);\n }\n\n const actual_pct = total_value === 0 ? 0 : (h.current_value / total_value) * 100;\n\n return {\n ...h,\n actual_pct,\n target_pct: Number(target.target_pct),\n min_pct: Number(target.min_pct),\n max_pct: Number(target.max_pct)\n };\n});\n\nreturn [{\n json: {\n holdings: assets,\n total_value,\n config\n }\n}];"
},
"typeVersion": 2
},
{
"id": "15978e61-2607-442d-9004-63d5aa829bb8",
"name": "Detect Portfolio Drift",
"type": "n8n-nodes-base.code",
"position": [
5136,
4240
],
"parameters": {
"jsCode": "const data = $input.first().json;\nconst holdings = data.holdings;\nconst total_value = data.total_value;\nconst config = data.config;\n\nlet rebalance_needed = false;\n\nconst assets = holdings.map(h => {\n const deviation_pct = h.actual_pct - h.target_pct;\n const target_value = total_value * (h.target_pct / 100);\n const adjustment_amount = Math.abs(h.current_value - target_value);\n\n let status = 'within_range';\n let suggested_action = 'Hold';\n\n if (h.actual_pct > h.max_pct) {\n status = 'overweight';\n suggested_action = 'Reduce';\n rebalance_needed = true;\n } else if (h.actual_pct < h.min_pct) {\n status = 'underweight';\n suggested_action = 'Increase';\n rebalance_needed = true;\n }\n\n return {\n ...h,\n deviation_pct,\n target_value,\n adjustment_amount,\n status,\n suggested_action\n };\n});\n\nconst affected_assets = assets\n .filter(a => a.status !== 'within_range')\n .map(a => a.asset);\n\nreturn [{\n json: {\n total_value,\n assets,\n affected_assets,\n rebalance_needed,\n config\n }\n}];"
},
"typeVersion": 2
},
{
"id": "3d0e7dce-1d90-44c7-8751-5c83c27b46bb",
"name": "Check Rebalance Requirement",
"type": "n8n-nodes-base.if",
"position": [
5360,
4240
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.rebalance_needed }}",
"rightValue": true
}
]
}
},
"typeVersion": 2.3
},
{
"id": "ec658cb8-383f-43c4-8c95-3717f711a94e",
"name": "Classify Alert Severity",
"type": "n8n-nodes-base.code",
"position": [
5584,
4144
],
"parameters": {
"jsCode": "const data = $input.first().json;\nconst assets = data.assets;\nconst config = data.config;\n\nconst maxDeviation = Math.max(...assets.map(a => Math.abs(a.deviation_pct)));\n\nlet severity = 'low';\nif (maxDeviation > Number(config.severity_rules.medium_max)) {\n severity = 'high';\n} else if (maxDeviation > Number(config.severity_rules.low_max)) {\n severity = 'medium';\n}\n\nreturn [{\n json: {\n ...data,\n severity,\n max_deviation: maxDeviation\n }\n}];"
},
"typeVersion": 2
},
{
"id": "622f0d6d-17b4-4ec0-ab5f-1f67ddd90b95",
"name": "Build Alert Prompt",
"type": "n8n-nodes-base.code",
"position": [
5808,
4144
],
"parameters": {
"jsCode": "const data = $input.first().json;\n\nconst affected = data.assets\n .filter(a => a.status !== 'within_range')\n .map(a => `${a.asset}: ${a.status}, actual ${a.actual_pct.toFixed(2)}%, target ${a.target_pct.toFixed(2)}%, deviation ${a.deviation_pct.toFixed(2)}%, action ${a.suggested_action}, amount ${data.config.currency} ${a.adjustment_amount.toFixed(2)}`)\n .join('\\n');\n\nconst ai_input = `\nYou are a financial portfolio monitoring assistant.\n\nYour task is to convert structured portfolio rebalance data into a professional alert message.\n\nSTRICT RULES:\n- Use only the numbers provided below\n- Do NOT recalculate anything\n- Do NOT invent any values\n- Keep the message concise, clear and professional\n- Use plain text only\n- Mention severity naturally\n\nDATA:\nPortfolio Name: ${data.config.portfolio_name}\nCurrency: ${data.config.currency}\nTotal Portfolio Value: ${data.total_value}\nSeverity: ${data.severity}\nMaximum Deviation: ${data.max_deviation.toFixed(2)}%\n\nAssets requiring rebalance:\n${affected}\n\nOUTPUT FORMAT:\nWrite a clean paragraph-style alert message suitable for email.\n`;\n\nreturn [{\n json: {\n ...data,\n ai_input\n }\n}];"
},
"typeVersion": 2
},
{
"id": "9379e3af-ce16-481c-9def-ca9541d41667",
"name": "Generate Alert Message",
"type": "@n8n/n8n-nodes-langchain.googleGemini",
"position": [
5952,
4064
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "models/gemini-3.1-flash-lite-preview",
"cachedResultName": "models/gemini-3.1-flash-lite-preview"
},
"options": {},
"messages": {
"values": [
{
"content": "={{ $json.ai_input }}"
}
]
},
"builtInTools": {}
},
"credentials": {
"googlePalmApi": {
"id": "credential-id",
"name": "Google Gemini(PaLM) Api account"
}
},
"typeVersion": 1.1
},
{
"id": "77a77ce0-4569-4c61-bb8c-cf7a1d885dcd",
"name": "Merge Alert Data",
"type": "n8n-nodes-base.merge",
"position": [
6256,
4128
],
"parameters": {
"mode": "combine",
"options": {},
"combineBy": "combineByPosition"
},
"typeVersion": 3.2
},
{
"id": "a8d39754-f8b5-4cab-a8f2-819ceec55b46",
"name": "Prepare Alert Payload",
"type": "n8n-nodes-base.set",
"position": [
6432,
4128
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "3de685c8-ff1f-43a1-870d-e58542b7596b",
"name": "alert_message",
"type": "string",
"value": "={{ $json.content.parts[0].text }}"
},
{
"id": "fbc8f85a-24b4-40d0-9981-e4b85f38c6a1",
"name": "email_to",
"type": "string",
"value": "={{ $json.config.alert_email }}"
},
{
"id": "dc6b09bd-6745-4110-8ec4-2322014da2a1",
"name": "email_subject",
"type": "string",
"value": "=Commodity Portfolio Alert - {{$json.severity.toUpperCase()}}"
},
{
"id": "c715d84c-f0c7-47c5-a3ef-2dd0c2bccbd6",
"name": "affected_assets_text",
"type": "string",
"value": "={{ $json.affected_assets.join(', ') }}"
},
{
"id": "f4861430-940f-48b2-be93-2d810c5ada97",
"name": "run_date",
"type": "string",
"value": "={{ $now }}"
},
{
"id": "aef321d5-2581-43e2-85db-42cdb7515f16",
"name": "alert_sent",
"type": "string",
"value": "yes"
},
{
"id": "43b07038-8485-4efa-9c06-7d0eee590f5e",
"name": "total_value",
"type": "number",
"value": "={{ $json.total_value }}"
},
{
"id": "24388c93-02d0-4fa8-b84f-83a7f1fca40b",
"name": "severity",
"type": "string",
"value": "={{ $json.severity }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "708ee600-4e9a-4265-bbf1-3c105dbf080f",
"name": "Send Rebalance Email",
"type": "n8n-nodes-base.gmail",
"position": [
6608,
4048
],
"webhookId": "6221c310-de84-4f47-bce1-c66d8565e0d7",
"parameters": {
"sendTo": "={{ $('Prepare Alert Payload').item.json.email_to }}",
"message": "={{ $('Prepare Alert Payload').item.json.alert_message + '\\n\\nAffected Assets: ' + $('Prepare Alert Payload').item.json.affected_assets_text + '\\nSeverity: ' + $('Prepare Alert Payload').item.json.severity + '\\nTotal Portfolio Value: ' + $('Prepare Alert Payload').item.json.total_value + '\\nRun Date: ' + $('Prepare Alert Payload').item.json.run_date }}",
"options": {},
"subject": "={{ $('Prepare Alert Payload').item.json.email_subject }}"
},
"credentials": {
"gmailOAuth2": {
"id": "credential-id",
"name": "Gmail account"
}
},
"typeVersion": 2.2
},
{
"id": "f6e4ac7b-b17d-4d3e-91f4-4656880e1a4a",
"name": "Log Sent Alert",
"type": "n8n-nodes-base.googleSheets",
"position": [
6608,
4224
],
"parameters": {
"columns": {
"value": {
"summary": "={{ $json.alert_message }}",
"run_date": "={{ $json.run_date }}",
"severity": "={{ $json.severity }}",
"alert_sent": "={{ $json.alert_sent }}",
"total_value": "={{ $json.total_value }}",
"affected_assets": "={{ $json.affected_assets_text }}",
"rebalance_needed": "yes"
},
"schema": [
{
"id": "run_date",
"type": "string",
"display": true,
"required": false,
"displayName": "run_date",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "total_value",
"type": "string",
"display": true,
"required": false,
"displayName": "total_value",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "rebalance_needed",
"type": "string",
"display": true,
"required": false,
"displayName": "rebalance_needed",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "severity",
"type": "string",
"display": true,
"required": false,
"displayName": "severity",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "affected_assets",
"type": "string",
"display": true,
"required": false,
"displayName": "affected_assets",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "alert_sent",
"type": "string",
"display": true,
"required": false,
"displayName": "alert_sent",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "summary",
"type": "string",
"display": true,
"required": false,
"displayName": "summary",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "reason_skipped",
"type": "string",
"display": true,
"required": false,
"displayName": "reason_skipped",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "list",
"value": 347522276,
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1_lcrJ7b0c9ZqyenXZZZ-A5Yyb99lS-k8qSs-Y9PV3EA/edit#gid=347522276",
"cachedResultName": "rebalance_log"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1_lcrJ7b0c9ZqyenXZZZ-A5Yyb99lS-k8qSs-Y9PV3EA",
"cachedResultName": "Commodity Portfolio"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"id": "credential-id",
"name": "Google Sheets account"
}
},
"typeVersion": 4.7
},
{
"id": "91883dee-8be2-4ed6-93a8-802dda291517",
"name": "Prepare Validation Failure Log",
"type": "n8n-nodes-base.set",
"position": [
4928,
4512
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"name": "run_date",
"type": "string",
"value": "={{ $now }}"
},
{
"name": "total_value",
"type": "string",
"value": ""
},
{
"name": "rebalance_needed",
"type": "string",
"value": "no"
},
{
"name": "severity",
"type": "string",
"value": "validation_failed"
},
{
"name": "affected_assets",
"type": "string",
"value": ""
},
{
"name": "alert_sent",
"type": "string",
"value": "no"
},
{
"name": "summary",
"type": "string",
"value": "Validation failed before allocation analysis."
},
{
"name": "reason_skipped",
"type": "string",
"value": "={{ $json.error }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "5300f585-5b9a-42c8-88ca-b3cb8766ac80",
"name": "Log Validation Failure",
"type": "n8n-nodes-base.googleSheets",
"position": [
5136,
4512
],
"parameters": {
"columns": {
"value": {
"summary": "={{ $json.summary }}",
"run_date": "={{ $json.run_date }}",
"severity": "={{ $json.severity }}",
"alert_sent": "={{ $json.alert_sent }}",
"total_value": "={{ $json.total_value }}",
"reason_skipped": "={{ $json.reason_skipped }}",
"affected_assets": "={{ $json.affected_assets }}",
"rebalance_needed": "={{ $json.rebalance_needed }}"
},
"mappingMode": "defineBelow"
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "list",
"value": 347522276,
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1_lcrJ7b0c9ZqyenXZZZ-A5Yyb99lS-k8qSs-Y9PV3EA/edit#gid=347522276",
"cachedResultName": "rebalance_log"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1_lcrJ7b0c9ZqyenXZZZ-A5Yyb99lS-k8qSs-Y9PV3EA",
"cachedResultName": "Commodity Portfolio"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"id": "credential-id",
"name": "Google Sheets account"
}
},
"typeVersion": 4.7
},
{
"id": "c899fa73-de9a-4814-ab75-71489c8da0d2",
"name": "Prepare No Action Log",
"type": "n8n-nodes-base.set",
"position": [
5584,
4304
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"name": "run_date",
"type": "string",
"value": "={{ $now }}"
},
{
"name": "total_value",
"type": "string",
"value": "={{ $json.total_value }}"
},
{
"name": "rebalance_needed",
"type": "string",
"value": "no"
},
{
"name": "severity",
"type": "string",
"value": "none"
},
{
"name": "affected_assets",
"type": "string",
"value": ""
},
{
"name": "alert_sent",
"type": "string",
"value": "no"
},
{
"name": "summary",
"type": "string",
"value": "No drift detected. Portfolio is within target range."
},
{
"name": "reason_skipped",
"type": "string",
"value": "No rebalance needed."
}
]
}
},
"typeVersion": 3.4
},
{
"id": "e62048b2-5b87-4166-84cb-25109dcdfdfd",
"name": "Log No Action",
"type": "n8n-nodes-base.googleSheets",
"position": [
5808,
4304
],
"parameters": {
"columns": {
"value": {
"summary": "={{ $json.summary }}",
"run_date": "={{ $json.run_date }}",
"severity": "={{ $json.severity }}",
"alert_sent": "={{ $json.alert_sent }}",
"total_value": "={{ $json.total_value }}",
"reason_skipped": "={{ $json.reason_skipped }}",
"affected_assets": "={{ $json.affected_assets }}",
"rebalance_needed": "={{ $json.rebalance_needed }}"
},
"schema": [
{
"id": "run_date",
"type": "string",
"display": true,
"required": false,
"displayName": "run_date",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "total_value",
"type": "string",
"display": true,
"required": false,
"displayName": "total_value",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "rebalance_needed",
"type": "string",
"display": true,
"required": false,
"displayName": "rebalance_needed",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "severity",
"type": "string",
"display": true,
"required": false,
"displayName": "severity",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "affected_assets",
"type": "string",
"display": true,
"required": false,
"displayName": "affected_assets",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "alert_sent",
"type": "string",
"display": true,
"required": false,
"displayName": "alert_sent",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "summary",
"type": "string",
"display": true,
"required": false,
"displayName": "summary",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "reason_skipped",
"type": "string",
"display": true,
"required": false,
"displayName": "reason_skipped",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "list",
"value": 347522276,
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1_lcrJ7b0c9ZqyenXZZZ-A5Yyb99lS-k8qSs-Y9PV3EA/edit#gid=347522276",
"cachedResultName": "rebalance_log"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1_lcrJ7b0c9ZqyenXZZZ-A5Yyb99lS-k8qSs-Y9PV3EA",
"cachedResultName": "Commodity Portfolio"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"id": "credential-id",
"name": "Google Sheets account"
}
},
"typeVersion": 4.7
},
{
"id": "0695d34f-1ed2-4f48-8d5b-07fd7ae07158",
"name": "Daily Portfolio Rebalance Check",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
3488,
4352
],
"parameters": {
"rule": {
"interval": [
{
"triggerAtHour": 9
}
]
}
},
"typeVersion": 1.3
},
{
"parameters": {
"operation": "verify",
"email": "={{ $('Prepare Alert Payload').item.json.email_to }}",
"additionalOptions": {}
},
"type": "n8n-nodes-billionverify.billionVerify",
"typeVersion": 1,
"position": [
6248,
4048
],
"name": "Verify Email (BillionVerify)",
"credentials": {
"billionVerifyApi": {
"id": "",
"name": "BillionVerify account"
}
}
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "is-deliverable",
"leftValue": "={{ $json.is_deliverable }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
]
}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
6428,
4048
],
"name": "IF deliverable"
}
],
"connections": {
"Read Holdings": {
"main": [
[
{
"node": "Synchronize Inputs",
"type": "main",
"index": 1
}
]
]
},
"Log Sent Alert": {
"main": [
[]
]
},
"Merge Alert Data": {
"main": [
[
{
"node": "Prepare Alert Payload",
"type": "main",
"index": 0
}
]
]
},
"Workflow Settings": {
"main": [
[
{
"node": "Synchronize Inputs",
"type": "main",
"index": 0
}
]
]
},
"Build Alert Prompt": {
"main": [
[
{
"node": "Generate Alert Message",
"type": "main",
"index": 0
},
{
"node": "Merge Alert Data",
"type": "main",
"index": 1
}
]
]
},
"Synchronize Inputs": {
"main": [
[
{
"node": "Prepare Portfolio Context",
"type": "main",
"index": 0
}
]
]
},
"Send Rebalance Email": {
"main": [
[]
]
},
"Prepare Alert Payload": {
"main": [
[
{
"node": "Log Sent Alert",
"type": "main",
"index": 0
},
{
"node": "Verify Email (BillionVerify)",
"type": "main",
"index": 0
}
]
]
},
"Prepare No Action Log": {
"main": [
[
{
"node": "Log No Action",
"type": "main",
"index": 0
}
]
]
},
"Detect Portfolio Drift": {
"main": [
[
{
"node": "Check Rebalance Requirement",
"type": "main",
"index": 0
}
]
]
},
"Generate Alert Message": {
"main": [
[
{
"node": "Merge Alert Data",
"type": "main",
"index": 0
}
]
]
},
"Check Validation Status": {
"main": [
[
{
"node": "Calculate Portfolio Allocation",
"type": "main",
"index": 0
}
],
[
{
"node": "Prepare Validation Failure Log",
"type": "main",
"index": 0
}
]
]
},
"Classify Alert Severity": {
"main": [
[
{
"node": "Build Alert Prompt",
"type": "main",
"index": 0
}
]
]
},
"Validate Portfolio Data": {
"main": [
[
{
"node": "Check Validation Status",
"type": "main",
"index": 0
}
]
]
},
"Prepare Portfolio Context": {
"main": [
[
{
"node": "Validate Portfolio Data",
"type": "main",
"index": 0
}
]
]
},
"Check Rebalance Requirement": {
"main": [
[
{
"node": "Classify Alert Severity",
"type": "main",
"index": 0
}
],
[
{
"node": "Prepare No Action Log",
"type": "main",
"index": 0
}
]
]
},
"Calculate Portfolio Allocation": {
"main": [
[
{
"node": "Detect Portfolio Drift",
"type": "main",
"index": 0
}
]
]
},
"Prepare Validation Failure Log": {
"main": [
[
{
"node": "Log Validation Failure",
"type": "main",
"index": 0
}
]
]
},
"Daily Portfolio Rebalance Check": {
"main": [
[
{
"node": "Read Holdings",
"type": "main",
"index": 0
},
{
"node": "Workflow Settings",
"type": "main",
"index": 0
}
]
]
},
"Verify Email (BillionVerify)": {
"main": [
[
{
"node": "IF deliverable",
"type": "main",
"index": 0
}
]
]
},
"IF deliverable": {
"main": [
[
{
"node": "Send Rebalance Email",
"type": "main",
"index": 0
}
],
[]
]
}
},
"settings": {
"binaryMode": "separate",
"executionOrder": "v1"
}
}When to use this
- Cleaning a list before a Drift send or sync.
- Protecting Drift deliverability and sender reputation.
- Keeping bounce rates low so your sending is never throttled.
FAQ
Why verify before sending in Drift?
Verifying first keeps your bounce rate low, which protects your sender reputation and your results.
How do I import this workflow?
Download the JSON, then in n8n go to Workflows → Import from File (or paste it). Install the BillionVerify community node and add your API key credential.
What happens to risky or catch-all addresses?
They are routed to the false branch and excluded from the send. You decide whether to retry, review, or drop them.
Add verification to your workflow
Create a free account, grab your API key, and stop bounces before they happen.
Get started free