This workflow corresponds to n8n.io template #15850 — we link there as the canonical source.
This workflow follows the Agent → 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": "YmwrQrEFrfBvbhOA",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Portfolio Exposure Risk Summary Generator",
"tags": [],
"nodes": [
{
"id": "37ad2b30-9277-413d-826d-09e9efaf0f69",
"name": "Schedule Risk Review",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-32,
96
],
"parameters": {
"rule": {
"interval": [
{
"field": "weeks",
"triggerAtDay": [
1
],
"triggerAtHour": 9
}
]
}
},
"typeVersion": 1.3
},
{
"id": "51ca57b3-0231-4822-8f91-a1c841cc9e68",
"name": "Read Portfolio From Sheet",
"type": "n8n-nodes-base.googleSheets",
"position": [
192,
96
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1ULRXPrhiQmWLQ51qRtCThurSMZefkjNOKxGe2pYh7Yw/edit#gid=0",
"cachedResultName": "Portfolio Data"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1ULRXPrhiQmWLQ51qRtCThurSMZefkjNOKxGe2pYh7Yw",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1ULRXPrhiQmWLQ51qRtCThurSMZefkjNOKxGe2pYh7Yw/edit?usp=drivesdk",
"cachedResultName": "Portfolio"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7,
"alwaysOutputData": true
},
{
"id": "0be694ca-0225-4097-a6e7-bd91f20bbf3a",
"name": "Check Portfolio Data Exists",
"type": "n8n-nodes-base.if",
"position": [
432,
96
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "0b90c68c-54b1-480f-a332-66046f2e14d2",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{ $json[\"Asset Name\"] }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.3
},
{
"id": "aa7b1b25-5cbe-4b77-b8fd-15ab5d15fa22",
"name": "Calculate Portfolio Exposure Metrics",
"type": "n8n-nodes-base.code",
"position": [
784,
-48
],
"parameters": {
"jsCode": "const items = $input.all();\n\n// Clean and normalize data\nconst portfolio = items\n .map(item => {\n const quantity = Number(item.json[\"Quantity\"] || 0);\n const price = Number(item.json[\"Price\"] || 0);\n const marketValue = item.json[\"Market Value\"]\n ? Number(item.json[\"Market Value\"])\n : quantity * price;\n\n return {\n name: item.json[\"Asset Name\"],\n sector: item.json[\"Sector\"],\n value: marketValue\n };\n })\n .filter(p => p.name && p.value > 0);\n\n// Total portfolio value\nconst totalValue = portfolio.reduce((sum, p) => sum + p.value, 0);\n\n// Sector Exposure\nconst sectorMap = {};\nfor (const p of portfolio) {\n if (!sectorMap[p.sector]) {\n sectorMap[p.sector] = 0;\n }\n sectorMap[p.sector] += p.value;\n}\n\nconst sectorExposure = Object.entries(sectorMap).map(([sector, value]) => ({\n sector,\n value,\n percentage: ((value / totalValue) * 100).toFixed(2)\n}));\n\n// Sort sector exposure (highest first)\nsectorExposure.sort((a, b) => b.value - a.value);\n\n// Sort holdings\nconst sortedHoldings = [...portfolio].sort((a, b) => b.value - a.value);\n\n// Concentration (Top Holding)\nconst topHolding = sortedHoldings[0];\nconst concentrationPercent = ((topHolding.value / totalValue) * 100).toFixed(2);\n\n// Top 3 holdings (for context)\nconst topHoldings = sortedHoldings.slice(0, 3).map(h => ({\n name: h.name,\n value: h.value,\n percentage: ((h.value / totalValue) * 100).toFixed(2)\n}));\n\nreturn [\n {\n json: {\n total_value: totalValue,\n\n sector_exposure: sectorExposure,\n\n concentration: {\n top_holding_name: topHolding.name,\n top_holding_value: topHolding.value,\n concentration_percentage: concentrationPercent\n },\n\n top_holdings: topHoldings\n }\n }\n];"
},
"typeVersion": 2
},
{
"id": "8dcde1e8-c6cc-464d-a28d-a00f31e417f9",
"name": "Google Gemini Chat Model",
"type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
"position": [
992,
112
],
"parameters": {
"options": {},
"modelName": "models/gemini-flash-lite-latest"
},
"credentials": {
"googlePalmApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "b742c761-f7d5-4626-8cf9-dcb7ce19b1c0",
"name": "Generate Risk Summary",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
992,
-48
],
"parameters": {
"text": "=You are a financial risk analyst.\n\nAnalyze the following portfolio exposure metrics and generate a concise professional risk summary.\n\nTotal Portfolio Value:\n{{ $json.total_value }}\n\nSector Exposure:\n{{ JSON.stringify($json.sector_exposure, null, 2) }}\n\nConcentration:\n{{ JSON.stringify($json.concentration, null, 2) }}\n\nTop Holdings:\n{{ JSON.stringify($json.top_holdings, null, 2) }}\n\nYour task:\n- Summarize the key portfolio risks\n- Mention whether sector allocation appears concentrated\n- Mention whether holding concentration is high\n- Briefly comment on diversification\n- Suggest 1\u20132 practical observations\n\nRules:\n- Keep the response concise\n- Use professional language\n- Do not exaggerate\n- Focus only on the data provided\n\nReturn the result in valid JSON only, using this format:\n{\n \"risk_summary\": \"...\"\n}",
"options": {},
"promptType": "define"
},
"typeVersion": 3.1
},
{
"id": "8ea911c9-b16f-4c68-a3a4-268fbd68354b",
"name": "Parse Risk Summary",
"type": "n8n-nodes-base.code",
"position": [
1344,
-48
],
"parameters": {
"jsCode": "const raw = $json.output || '{}';\n\nlet parsed;\n\ntry {\n parsed = JSON.parse(raw);\n} catch (error) {\n parsed = {\n risk_summary: raw\n };\n}\n\nreturn [\n {\n json: {\n risk_summary: parsed.risk_summary || ''\n }\n }\n];"
},
"typeVersion": 2
},
{
"id": "8dc01576-77ed-4a17-b369-0e3c100bd1de",
"name": "Prepare Risk Report Data",
"type": "n8n-nodes-base.set",
"position": [
1728,
16
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "a728bc5c-e6a1-4298-8407-d797f885dfd6",
"name": "total_value",
"type": "number",
"value": "={{ $('Calculate Portfolio Exposure Metrics').first().json.total_value }}"
},
{
"id": "cf889c33-1665-4bf9-8a42-82a1a540387c",
"name": "top_sector",
"type": "string",
"value": "={{ $('Calculate Portfolio Exposure Metrics').first().json.sector_exposure[0].sector }}"
},
{
"id": "f3061a64-4db5-4d52-aed2-b26e0b85864f",
"name": "top_sector_exposure",
"type": "string",
"value": "={{ $('Calculate Portfolio Exposure Metrics').first().json.sector_exposure[0].percentage }}"
},
{
"id": "d4cf083f-6ca9-4364-8e64-1cf79092600b",
"name": "top_holding",
"type": "string",
"value": "={{ $('Calculate Portfolio Exposure Metrics').first().json.concentration.top_holding_name }}"
},
{
"id": "49edffea-1d72-4374-a281-2a09c9a48953",
"name": "concentration_percentage",
"type": "number",
"value": "={{ $('Calculate Portfolio Exposure Metrics').first().json.concentration.concentration_percentage }}"
},
{
"id": "38b0c909-897b-4955-aebe-102a290d82b2",
"name": "risk_summary",
"type": "string",
"value": "={{ $json.risk_summary }}"
},
{
"id": "9694c1a1-74c6-4995-86ea-9cfaf0c19b6b",
"name": "id",
"type": "string",
"value": "={{ $now.toFormat('yyyyLLddHHmmss') }}"
},
{
"id": "09d9dea1-2949-463e-8253-74de30eca1b2",
"name": "run_date",
"type": "string",
"value": "={{ $now.toFormat('dd LLL yyyy, hh:mm a') }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "1bca40dd-f01f-46b6-b6a8-9f68163a6863",
"name": "Send Risk Report Email",
"type": "n8n-nodes-base.gmail",
"position": [
2144,
-208
],
"parameters": {
"sendTo": "",
"message": "=<h2>Portfolio Risk Summary</h2>\n\n<p><strong>Total Portfolio Value:</strong> \u20b9{{ $json.total_value }}</p>\n<p><strong>Top Sector Exposure:</strong> {{ $json.top_sector }} ({{ $json.top_sector_exposure }}%)</p>\n<p><strong>Top Holding:</strong> {{ $json.top_holding }} ({{ $json.concentration_percentage }}%)</p>\n\n<h3>Risk Summary</h3>\n<p>{{ $json.risk_summary }}</p>",
"options": {
"appendAttribution": false
},
"subject": "={{ 'Portfolio Risk Summary Report - ' + $now.toFormat('dd LLL yyyy') }}"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.2
},
{
"id": "b5563565-3267-43a4-8687-2072ed75c8b1",
"name": "Log Risk Review Result",
"type": "n8n-nodes-base.googleSheets",
"position": [
2144,
256
],
"parameters": {
"columns": {
"value": {
"Run Date": "={{ $json.run_date }}",
"Top Sector": "={{ $json.top_sector }}",
"Top Holding": "={{ $json.top_holding }}",
"Risk Summary": "={{ $json.risk_summary }}",
"Top Sector Exposure": "={{ $json.top_sector_exposure }}",
"Total Portfolio Value": "={{ $json.total_value }}",
"Concentration Percentage": "={{ $json.concentration_percentage }}"
},
"schema": [
{
"id": "ID",
"type": "string",
"display": true,
"required": false,
"displayName": "ID",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Run Date",
"type": "string",
"display": true,
"required": false,
"displayName": "Run Date",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Total Portfolio Value",
"type": "string",
"display": true,
"required": false,
"displayName": "Total Portfolio Value",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Top Sector",
"type": "string",
"display": true,
"required": false,
"displayName": "Top Sector",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Top Sector Exposure",
"type": "string",
"display": true,
"required": false,
"displayName": "Top Sector Exposure",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Top Holding",
"type": "string",
"display": true,
"required": false,
"displayName": "Top Holding",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Concentration Percentage",
"type": "string",
"display": true,
"required": false,
"displayName": "Concentration Percentage",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Risk Summary",
"type": "string",
"display": true,
"required": false,
"displayName": "Risk Summary",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "list",
"value": 1461766410,
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1ULRXPrhiQmWLQ51qRtCThurSMZefkjNOKxGe2pYh7Yw/edit#gid=1461766410",
"cachedResultName": "Risk Review Log"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1ULRXPrhiQmWLQ51qRtCThurSMZefkjNOKxGe2pYh7Yw",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1ULRXPrhiQmWLQ51qRtCThurSMZefkjNOKxGe2pYh7Yw/edit?usp=drivesdk",
"cachedResultName": "Portfolio"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "1d7bf6a2-14b3-4eae-a611-a537d8ae3141",
"name": "Send Risk Report to Slack",
"type": "n8n-nodes-base.slack",
"position": [
2144,
16
],
"parameters": {
"text": "=*Portfolio Risk Summary Report*\n\n*Total Portfolio Value:* \u20b9{{ $json.total_value }}\n*Top Sector Exposure:* {{ $json.top_sector }} ({{ $json.top_sector_exposure }}%)\n*Top Holding:* {{ $json.top_holding }} ({{ $json.concentration_percentage }}%)\n\n*Risk Summary:*\n{{ $json.risk_summary }}",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C0AR6V3F0UV",
"cachedResultName": "risk-reports"
},
"otherOptions": {
"includeLinkToWorkflow": false
},
"authentication": "oAuth2"
},
"credentials": {
"slackOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 2.4
},
{
"id": "aea313d2-6911-4394-ad70-ae1f4b0e38d7",
"name": "Prepare Empty Portfolio Log",
"type": "n8n-nodes-base.set",
"position": [
1728,
352
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "378ee139-401c-4e8e-983c-97787c15e1c4",
"name": "run_date",
"type": "string",
"value": "={{ $now.toFormat('dd LLL yyyy, hh:mm a') }}"
},
{
"id": "80334845-4294-4f19-949d-064638d5b206",
"name": "=total_value",
"type": "number",
"value": 0
},
{
"id": "6c617144-d6d3-4891-90ea-dfccee4fa583",
"name": "top_sector",
"type": "string",
"value": "N/A"
},
{
"id": "7eef4457-20ce-489b-a447-271804301b22",
"name": "top_sector_exposure",
"type": "number",
"value": 0
},
{
"id": "a947e049-76d5-40fc-8116-e6bb84584853",
"name": "top_holding",
"type": "string",
"value": "N/A"
},
{
"id": "a3ea4570-40cc-4a3d-9bfd-af53082aa3f4",
"name": "concentration_percentage",
"type": "number",
"value": 0
},
{
"id": "d7138063-3c31-4e3e-ae6c-786e88b0402f",
"name": "risk_summary",
"type": "string",
"value": "No portfolio data available. Risk analysis was not performed."
}
]
}
},
"typeVersion": 3.4
},
{
"id": "62011130-eef6-475a-8c92-818b99bfbcc5",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-96,
-720
],
"parameters": {
"width": 752,
"height": 448,
"content": "# Portfolio Exposure Risk Summary Generator\n\n\n## How it works:\nThis workflow runs on a schedule to analyze portfolio risk. It fetches portfolio data from Google Sheets, validates the data and calculates exposure metrics such as total value, sector allocation and concentration. These metrics are passed to an AI model to generate a concise professional risk summary. The final report is then formatted and delivered via email and Slack, while also being logged into a Google Sheet for tracking. If no portfolio data is available, a fallback summary is generated and logged.\n\n## Setup steps:\n1. Connect Google Sheets containing portfolio data \n2. Ensure columns like Sr. No., Asset Name, Sector, Quantity, Price, Market Value or Geography exist \n3. Configure Gemini API credentials for AI analysis \n4. Connect Gmail and Slack for report delivery \n5. Set schedule trigger for periodic risk review \n6. Test workflow and verify logging + notifications "
},
"typeVersion": 1
},
{
"id": "1904146a-2b36-4795-85e7-62b77c0070b8",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-96,
-80
],
"parameters": {
"color": 7,
"width": 688,
"height": 384,
"content": "## Portfolio Data Ingestion & Validation\nTriggers scheduled risk review, reads portfolio data from Google Sheets and validates that required asset information exists before proceeding with further analysis or fallback handling."
},
"typeVersion": 1
},
{
"id": "8c4e2506-fb2a-48bb-b623-7b7195263a7c",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
688,
-176
],
"parameters": {
"color": 7,
"width": 848,
"height": 448,
"content": "## Risk Analysis & AI Summary\nCalculates portfolio exposure metrics such as total value, sector allocation and concentration. These insights are passed to AI to generate a risk summary, which is then parsed and structured for downstream reporting."
},
"typeVersion": 1
},
{
"id": "65e00cb8-64dd-430a-be96-824f1c367c40",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
1616,
-352
],
"parameters": {
"color": 7,
"width": 752,
"height": 912,
"content": "## Reporting, Logging & Fallback\nFormats the final risk report, sends notifications via email and Slack and logs results into Google Sheets. If no portfolio data is available, a structured default report is prepared and logged to maintain consistent output."
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"binaryMode": "separate",
"executionOrder": "v1"
},
"versionId": "f2d3cbae-8d60-408c-b53b-29c3f636839b",
"connections": {
"Parse Risk Summary": {
"main": [
[
{
"node": "Prepare Risk Report Data",
"type": "main",
"index": 0
}
]
]
},
"Schedule Risk Review": {
"main": [
[
{
"node": "Read Portfolio From Sheet",
"type": "main",
"index": 0
}
]
]
},
"Generate Risk Summary": {
"main": [
[
{
"node": "Parse Risk Summary",
"type": "main",
"index": 0
}
]
]
},
"Send Risk Report Email": {
"main": [
[]
]
},
"Google Gemini Chat Model": {
"ai_languageModel": [
[
{
"node": "Generate Risk Summary",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Prepare Risk Report Data": {
"main": [
[
{
"node": "Send Risk Report Email",
"type": "main",
"index": 0
},
{
"node": "Send Risk Report to Slack",
"type": "main",
"index": 0
},
{
"node": "Log Risk Review Result",
"type": "main",
"index": 0
}
]
]
},
"Read Portfolio From Sheet": {
"main": [
[
{
"node": "Check Portfolio Data Exists",
"type": "main",
"index": 0
}
]
]
},
"Send Risk Report to Slack": {
"main": [
[]
]
},
"Check Portfolio Data Exists": {
"main": [
[
{
"node": "Calculate Portfolio Exposure Metrics",
"type": "main",
"index": 0
}
],
[
{
"node": "Prepare Empty Portfolio Log",
"type": "main",
"index": 0
}
]
]
},
"Prepare Empty Portfolio Log": {
"main": [
[
{
"node": "Log Risk Review Result",
"type": "main",
"index": 0
}
]
]
},
"Calculate Portfolio Exposure Metrics": {
"main": [
[
{
"node": "Generate Risk Summary",
"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.
gmailOAuth2googlePalmApigoogleSheetsOAuth2ApislackOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
> n8n + Google Sheets + Gemini + Slack + Gmail
Source: https://n8n.io/workflows/15850/ — 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.
Here’s a workflow that automates daily Slack notifications with a concise summary of unread emails (from the last 7 days) and Google Calendar events (for the current day). It integrates Gmail, Google
Created by: Peyton Leveillee Last updated: October 2025
The Multi-Model Agency Content Engine is a high-performance editorial system designed for agencies. It solves the "blank page" problem by alternating between real-world social proof and strategic expe
This workflow is the AI analysis and alerting engine for a complete social media monitoring system. It's designed to work with data scraped from X (formerly Twitter) using a tool like the Apify Tweet
Categories Content Creation AI Automation Publishing Social Media