{
  "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
          }
        ]
      ]
    }
  }
}