This workflow corresponds to n8n.io template #12487 — we link there as the canonical source.
This workflow follows the Error Trigger → 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": "ZdYSds4YyeJ0lg8Z",
"name": "Track portfolio performance and risk using Google Sheets and Alpha Vantage",
"tags": [],
"nodes": [
{
"id": "365a188d-baf8-4a34-a75a-82268fac6bb0",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-736,
-880
],
"parameters": {
"width": 544,
"height": 656,
"content": "## Track portfolio performance and risk using Google Sheets and Alpha Vantage - Overview\n\nThis workflow automatically tracks stock portfolio performance by reading holdings from Google Sheets, fetching daily market prices from Alpha Vantage, calculating returns and risk metrics, and updating the results back to the sheet. It also classifies each holding based on performance and drawdown.\n\n### How it works\n\nThe workflow starts on a schedule and reads portfolio data such as stock symbol, buy price, quantity, and buy date from Google Sheets. Each stock is processed one by one. For every holding, the workflow fetches recent price data from Alpha Vantage and calculates metrics like invested value, current value, profit and loss, return percentage, CAGR, and maximum drawdown.\nBased on return and drawdown, the workflow assigns a simple portfolio status (Healthy, Watch, or Risk), making the output easier to understand and act on. All calculated values are then written back to the sheet for tracking or dashboard use.\n\n### Setup steps\n\nCreate a Google Sheet tab named Portfolio Performance.\n\nConfigure Google Sheets and Alpha Vantage credentials.\n\nReplace the API key with your key before running the workflow."
},
"typeVersion": 1
},
{
"id": "529f71be-5a19-4d09-911e-f7dd4c18ad79",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-80,
-176
],
"parameters": {
"color": 7,
"width": 576,
"height": 368,
"content": "### Portfolio Input and Scheduling\n\nThis workflow runs on a schedule and reads stock holdings such as (symbol, buy price, quantity, buy date) from Google Sheets. Each row represents one portfolio position."
},
"typeVersion": 1
},
{
"id": "a8b67a41-3ae8-4a29-bbdf-6299e5ac4e1c",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
512,
-224
],
"parameters": {
"color": 7,
"width": 320,
"height": 416,
"content": "### Market data retrieval\n\nFetches daily stock price data from the Alpha Vantage API for each portfolio holding to ensure consistent performance calculations."
},
"typeVersion": 1
},
{
"id": "4a854d01-7cd7-4691-a618-6ea4b5f18345",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
848,
-224
],
"parameters": {
"color": 7,
"width": 608,
"height": 416,
"content": "### Performance and risk analysis\n\nCalculates invested value, current value, PnL, return %, CAGR, and maximum drawdown. Each stock is classified as Healthy, Watch, or Risk. After calculations, the latest values are written back to the same Google Sheet, updating existing rows using the stock symbol."
},
"typeVersion": 1
},
{
"id": "918eb914-c4f3-4aa1-8f4f-df25ef16a4c2",
"name": "Sticky Note8",
"type": "n8n-nodes-base.stickyNote",
"position": [
-512,
320
],
"parameters": {
"color": 7,
"width": 672,
"height": 336,
"content": "## \ud83d\udea8 Error Handling \n\n \nCatches any workflow failure and posts an alert to Email. \nIncludes node name, error message, and timestamp for quick debugging.\n"
},
"typeVersion": 1
},
{
"id": "b0920fc8-a89d-4728-bd26-a458496e8531",
"name": "Run daily portfolio update",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-48,
16
],
"parameters": {
"rule": {
"interval": [
{
"field": "hours",
"hoursInterval": 23,
"triggerAtMinute": 59
}
]
}
},
"typeVersion": 1.3
},
{
"id": "99515b62-d127-405d-8d43-dc1e71073f70",
"name": "Read portfolio holdings",
"type": "n8n-nodes-base.googleSheets",
"position": [
160,
16
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "list",
"value": 1013617369,
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Mz-woYDtXtzF2bA9IqpdYh28IPA76nFx46WHgDJOZoI/edit#gid=1013617369",
"cachedResultName": "Portfolio Performance"
},
"documentId": {
"__rl": true,
"mode": "url",
"value": "https://docs.google.com/spreadsheets/d/1Mz-woYDtXtzF2bA9IqpdYh28IPA76nFx46WHgDJOZoI/edit"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "7ffa2d8a-ea7d-4a6d-b7d5-c2cf70017e55",
"name": "Process each stock",
"type": "n8n-nodes-base.splitInBatches",
"position": [
352,
16
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "8dca0ca9-6d59-46e7-96b6-955a86799e6a",
"name": "Fetch daily stock prices",
"type": "n8n-nodes-base.httpRequest",
"position": [
624,
16
],
"parameters": {
"url": "https://www.alphavantage.co/query",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "function",
"value": "TIME_SERIES_DAILY"
},
{
"name": "symbol",
"value": "={{ $json.Stock }}"
},
{
"name": "apikey",
"value": "\"YOUR_ALPHA_VANTAGE_API_KEY\""
}
]
}
},
"typeVersion": 4.3
},
{
"id": "b32f194b-64cb-4892-88bb-d8fbc41f4017",
"name": "Calculate returns and risk metrics",
"type": "n8n-nodes-base.code",
"position": [
944,
16
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "// Extract latest close price\nconst timeSeries = $json['Time Series (Daily)'] || {};\nconst dates = Object.keys(timeSeries);\n\nif (!dates.length) {\n return {\n ...$json,\n CurrentPrice: null,\n InvestedValue: null,\n CurrentValue: null,\n PnL: null,\n PnLPercent: null,\n CAGR: null,\n LastUpdated: new Date().toISOString()\n };\n}\n\nconst latestDate = dates[0];\nconst currentPrice = parseFloat(timeSeries[latestDate]['4. close']);\n\nconst buyPrice = parseFloat($('Process each stock').item.json.BuyPrice);\nconst quantity = parseFloat($('Process each stock').item.json.Quantity);\nconst buyDate = new Date($('Process each stock').item.json.BuyDate);\nconst now = new Date();\n\nconst investedValue = buyPrice * quantity;\nconst currentValue = currentPrice * quantity;\nconst pnl = currentValue - investedValue;\nconst pnlPercent = (pnl / investedValue) * 100;\n\nconst yearsHeld = (now - buyDate) / (1000 * 60 * 60 * 24 * 365);\nconst cagr = yearsHeld > 0\n ? ((currentValue / investedValue) ** (1 / yearsHeld) - 1) * 100\n : null;\n\n\n// ----------------------------\n// MAX DRAWDOWN calculation\n// ----------------------------\nlet peak = -Infinity;\nlet maxDrawdown = 0;\n\n// Iterate oldest \u2192 newest\ndates.reverse().forEach(date => {\n const close = parseFloat(timeSeries[date]['4. close']);\n if (close > peak) {\n peak = close;\n }\n const drawdown = (close - peak) / peak;\n if (drawdown < maxDrawdown) {\n maxDrawdown = drawdown;\n }\n});\n\n// Convert to percentage\nmaxDrawdown = maxDrawdown * 100;\n\n\n// ----------------------------\n// Portfolio Health Classification\n// ----------------------------\nlet portfolioStatus = \"Watch\";\n\nif (cagr >= 15 && maxDrawdown > -20) {\n portfolioStatus = \"Healthy\";\n} else if (cagr < 5 || maxDrawdown <= -35) {\n portfolioStatus = \"Risk\";\n}\n\n\n\nreturn {\n Stock: $json[\"Meta Data\"][\"2. Symbol\"],\n BuyPrice: buyPrice,\n Quantity: quantity,\n BuyDate: $json.BuyDate,\n CurrentPrice: currentPrice,\n InvestedValue: investedValue,\n CurrentValue: currentValue,\n PnL: pnl,\n PnLPercent: pnlPercent,\n CAGR: cagr,\n MaxDrawdown: maxDrawdown,\n PortfolioStatus: portfolioStatus,\n LastUpdated: new Date().toISOString()\n};"
},
"typeVersion": 2
},
{
"id": "28bb1003-a7a0-4d83-a7c1-db8bab41e970",
"name": "Update portfolio performance",
"type": "n8n-nodes-base.googleSheets",
"position": [
1248,
16
],
"parameters": {
"columns": {
"value": {
"PnL": "={{ $json.PnL }}",
"CAGR": "={{ $json.CAGR }}",
"Stock": "={{ $json.Stock }}",
"PnLPercent": "={{ $json.PnLPercent }}",
"LastUpdated": "={{ $json.LastUpdated }}",
"MaxDrawdown": "={{ $json.MaxDrawdown }}",
"CurrentPrice": "={{ $json.CurrentPrice }}",
"CurrentValue": "={{ $json.CurrentValue }}",
"InvestedValue": "={{ $json.InvestedValue }}",
"PortfolioStatus": "={{ $json.PortfolioStatus }}"
},
"schema": [
{
"id": "Stock",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Stock",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "BuyPrice",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "BuyPrice",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Quantity",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "Quantity",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "BuyDate",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "BuyDate",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "CurrentPrice",
"type": "string",
"display": true,
"required": false,
"displayName": "CurrentPrice",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "InvestedValue",
"type": "string",
"display": true,
"required": false,
"displayName": "InvestedValue",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "CurrentValue",
"type": "string",
"display": true,
"required": false,
"displayName": "CurrentValue",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "PnL",
"type": "string",
"display": true,
"required": false,
"displayName": "PnL",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "PnLPercent",
"type": "string",
"display": true,
"required": false,
"displayName": "PnLPercent",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "CAGR",
"type": "string",
"display": true,
"required": false,
"displayName": "CAGR",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "MaxDrawdown",
"type": "string",
"display": true,
"required": false,
"displayName": "MaxDrawdown",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "LastUpdated",
"type": "string",
"display": true,
"required": false,
"displayName": "LastUpdated",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "PortfolioStatus",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "PortfolioStatus",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"Stock"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "appendOrUpdate",
"sheetName": {
"__rl": true,
"mode": "list",
"value": 1013617369,
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Mz-woYDtXtzF2bA9IqpdYh28IPA76nFx46WHgDJOZoI/edit#gid=1013617369",
"cachedResultName": "Portfolio Performance"
},
"documentId": {
"__rl": true,
"mode": "url",
"value": "https://docs.google.com/spreadsheets/d/1Mz-woYDtXtzF2bA9IqpdYh28IPA76nFx46WHgDJOZoI/edit"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "8a9f430e-88a1-4b9c-a99a-fceea5e6475b",
"name": "Workflow error trigger",
"type": "n8n-nodes-base.errorTrigger",
"position": [
-416,
480
],
"parameters": {},
"typeVersion": 1
},
{
"id": "b516ff82-6e22-453e-a0f5-9e871567ad06",
"name": "Send error notification email",
"type": "n8n-nodes-base.gmail",
"position": [
-96,
480
],
"parameters": {
"sendTo": "user@example.com",
"message": "=\u274c Error in Portfolio Performance and Risk Classification Tracker workflow, Node: {{ $json.node.name }} Message: {{ $json.error.message }} Time:{{ $json.timestamp }}",
"options": {},
"subject": "Error Alert \u26a0\ufe0f",
"emailType": "text"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.2
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "3fb3bcd7-bc6b-49a0-98b7-b5394837f9fb",
"connections": {
"Process each stock": {
"main": [
[],
[
{
"node": "Fetch daily stock prices",
"type": "main",
"index": 0
}
]
]
},
"Workflow error trigger": {
"main": [
[
{
"node": "Send error notification email",
"type": "main",
"index": 0
}
]
]
},
"Read portfolio holdings": {
"main": [
[
{
"node": "Process each stock",
"type": "main",
"index": 0
}
]
]
},
"Fetch daily stock prices": {
"main": [
[
{
"node": "Calculate returns and risk metrics",
"type": "main",
"index": 0
}
]
]
},
"Run daily portfolio update": {
"main": [
[
{
"node": "Read portfolio holdings",
"type": "main",
"index": 0
}
]
]
},
"Calculate returns and risk metrics": {
"main": [
[
{
"node": "Update portfolio performance",
"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.
gmailOAuth2googleSheetsOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow helps users track and understand the performance of their stock portfolio in an automated and structured way. It reads portfolio holdings from Google Sheets, fetches the latest market prices, calculates key performance metrics, and updates the results back into the…
Source: https://n8n.io/workflows/12487/ — 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 automation calculates commonly used technical indicators for selected stocks and presents the results in a simple, structured dashboard. It removes the need for manual chart analysis by automatic
YOUR_ID 4. Uses gmail, googleDrive, googleSheets, httpRequest. Scheduled trigger; 53 nodes.
Looking for a way to track GitHub bounty issues automatically and get notified in real time? This GitHub Bounty Tracker workflow monitors repositories for issues labeled 💎 Bounty, logs them in Google
This workflow automatically sends a beautifully designed HTML newsletter every Sunday at 8 AM, featuring products currently on sale from your Algolia-powered e-commerce store.
This n8n template demonstrates how to build a Auto Lead Gen & Outreach System for Local Businesses specifically designed to help businesses that don’t have a website yet.