This workflow corresponds to n8n.io template #12333 — we link there as the canonical source.
This workflow follows the Agent → Google Sheets 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": "l40Qu7DpgYM2JHVg",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "AI Stock Analysis for Buy/No-Buy Decisions Using Google Sheets & EODHD APIs",
"tags": [],
"nodes": [
{
"id": "903c0cd1-5734-4a4d-99de-03acaf28595c",
"name": "When clicking \u2018Execute workflow\u2019",
"type": "n8n-nodes-base.manualTrigger",
"position": [
-1024,
100
],
"parameters": {},
"typeVersion": 1
},
{
"id": "53f2218c-55ce-4b2e-964b-a119fdabc486",
"name": "Get row(s) in sheet",
"type": "n8n-nodes-base.googleSheets",
"position": [
-800,
100
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "YOUR_GOOGLE_SHEET_ID"
}
},
"typeVersion": 4.7
},
{
"id": "dad4cbe0-e5b3-417e-8bba-5adaaf1cb570",
"name": "Loop Over Items",
"type": "n8n-nodes-base.splitInBatches",
"position": [
-576,
100
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "edf3eaa2-5b9d-40f9-a0fe-b526fbc2c06b",
"name": "Merge",
"type": "n8n-nodes-base.merge",
"position": [
320,
-24
],
"parameters": {},
"typeVersion": 3.2
},
{
"id": "92132dbf-383f-47ca-a4e9-5a26d8631978",
"name": "OpenAI Chat Model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
840,
200
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "gpt-4.1-nano",
"cachedResultName": "gpt-4.1-nano"
},
"options": {},
"builtInTools": {}
},
"typeVersion": 1.3
},
{
"id": "ec661db6-824a-44e7-8e97-1e62bb472f1b",
"name": "Code in JavaScript2",
"type": "n8n-nodes-base.code",
"position": [
1120,
-16
],
"parameters": {
"jsCode": "// Parse AI output string -> JSON object\nvar raw = items[0].json.output;\n\nif (!raw || typeof raw !== 'string') {\n return [{ json: { error: 'No output string to parse', raw: raw } }];\n}\n\nvar obj = JSON.parse(raw);\n\n// Devuelve el objeto ya parseado\nreturn [{ json: obj }];\n"
},
"typeVersion": 2
},
{
"id": "cda8d21f-0612-41d7-a673-17d5abe93df7",
"name": "Append row in sheet",
"type": "n8n-nodes-base.googleSheets",
"position": [
1568,
100
],
"parameters": {
"columns": {
"value": {
"ENTRY": "={{ $json.entry }}",
"ticker": "={{ $json.ticker }}",
"support": "={{ $json.support }}",
"negative": "={{ $json.negatives }}",
"positives": "={{ $json.positives }}",
"stop_loss": "={{ $json.stop_loss }}",
"resistance": "={{ $json.resistance }}",
"take_profit": "={{ $json.take_profit }}",
"ENTER(YES/NO)": "={{ $json.would_enter }}",
"tecnhical_tesis": "={{ $json.rationale }}",
"fundamental_score": "={{ $json.fundamental_score_1_10 }}",
"fundamental_thesis": "={{ $json.thesis }}"
},
"schema": [
{
"id": "ticker",
"type": "string",
"display": true,
"required": false,
"displayName": "ticker",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "ENTER(YES/NO)",
"type": "string",
"display": true,
"required": false,
"displayName": "ENTER(YES/NO)",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "ENTRY",
"type": "string",
"display": true,
"required": false,
"displayName": "ENTRY",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "support",
"type": "string",
"display": true,
"required": false,
"displayName": "support",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "resistance",
"type": "string",
"display": true,
"required": false,
"displayName": "resistance",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "stop_loss",
"type": "string",
"display": true,
"required": false,
"displayName": "stop_loss",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "take_profit",
"type": "string",
"display": true,
"required": false,
"displayName": "take_profit",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "tecnhical_tesis",
"type": "string",
"display": true,
"required": false,
"displayName": "tecnhical_tesis",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "fundamental_score",
"type": "string",
"display": true,
"required": false,
"displayName": "fundamental_score",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "negative",
"type": "string",
"display": true,
"required": false,
"displayName": "negative",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "positives",
"type": "string",
"display": true,
"required": false,
"displayName": "positives",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "fundamental_thesis",
"type": "string",
"display": true,
"required": false,
"displayName": "fundamental_thesis",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "YOUR_GOOGLE_SHEET_ID"
}
},
"typeVersion": 4.7
},
{
"id": "518a7800-ba5a-4198-b637-9c506ba1ef20",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1568,
-928
],
"parameters": {
"width": 368,
"height": 1456,
"content": "## What this workflow does\nThis workflow automates **end-to-end stock analysis** using real market data and AI:\n\n- Reads a list of stock tickers from **Google Sheets**\n- Fetches **fundamental data** (valuation, growth, profitability) and **OHLCV price data** from **EODHD APIs**\n- Computes key **technical indicators** (RSI, SMA 20/50/200, volatility, support & resistance)\n- Uses an **AI model** to generate:\n - Buy / Watch / Sell recommendation\n - Entry price, stop-loss, and take-profit levels\n - Investment thesis, pros & cons\n - Fundamental quality score (1\u201310)\n- Stores the final structured analysis back into **Google Sheets**\n\nThis creates a **repeatable, no-code stock analysis pipeline** ready for decision-making or dashboards.\n\n### Data source\nMarket data is powered by **EODHD APIs** \n\ud83d\udc49 Get a **10% discount** using this link: \nhttps://eodhd.com/pricing-special-10?via=kmg&ref1=Meneses\n\n## How to configure this workflow\n\n### 1. Google Sheets (Input)\nCreate a sheet with a column called:\n- `ticker` (e.g. MSFT, AAPL, AMZN)\n\nEach row represents one stock to analyze.\n\n\n### 2. EODHD APIs\n- Create an EODHD account\n- Get your API token\n- Add it to the HTTP Request nodes as:\n - `api_token=YOUR_API_KEY`\n\nDiscount link (10% off): \nhttps://eodhd.com/pricing-special-10?via=kmg&ref1=Meneses\n\n\n### 3. AI Model\n- Configure your AI provider (OpenAI / compatible model)\n- The AI receives:\n - Fundamentals\n - Technical indicators\n - Growth potential score\n- It returns structured JSON with recommendations and trade levels\n\n\n### 4. Google Sheets (Output)\nResults are appended to a `Signals` tab with:\n- Signal (BUY / WATCH / SELL)\n- Entry, Stop Loss, Take Profit\n- Fundamental score (1\u201310)\n- Investment thesis and risk notes\n"
},
"typeVersion": 1
},
{
"id": "55aad4d8-f82c-48e9-8b7b-023c4e17d384",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1072,
-384
],
"parameters": {
"height": 336,
"content": "## INPUT\n\n\n"
},
"typeVersion": 1
},
{
"id": "a57c2bd7-9f41-459e-9773-a9099c1de4b4",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
1488,
-368
],
"parameters": {
"color": 7,
"width": 544,
"height": 224,
"content": "## AI Output\n- The merged data is sent to the **AI system**\n- The AI evaluates fundamentals, technicals, and risk\n- The final output includes:\n - BUY / WATCH / SELL signal\n - Entry, Stop Loss, Take Profit\n - Investment score and rationale\n\nResults are ready to use or store in Google Sheets."
},
"typeVersion": 1
},
{
"id": "2f850f00-2033-4cd8-95ea-25018a1d85dd",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
224,
-320
],
"parameters": {
"color": 7,
"width": 576,
"height": 192,
"content": "## Transform & Merge\n\n- Raw market data is cleaned and normalized\n- Technical indicators are calculated\n- Fundamentals and price data are **merged into one dataset**\n\nAt the end of this step, each stock has a single, structured data object.\n"
},
"typeVersion": 1
},
{
"id": "6d48267a-78c8-4a2f-945e-a51fff6527fa",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
-560,
-352
],
"parameters": {
"color": 7,
"width": 480,
"height": 192,
"content": "## Input (Data sources)\n- Stock tickers come from **Google Sheets**\n- Market data is fetched via **API calls**:\n - Fundamentals\n - Price candles (OHLCV)\n\nThis step collects all the raw data needed for the analysis."
},
"typeVersion": 1
},
{
"id": "98865136-2f82-442d-bb52-6c3972f2fc1f",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
1792,
112
],
"parameters": {
"width": 1008,
"height": 208,
"content": "## OUTPUT\n"
},
"typeVersion": 1
},
{
"id": "26f88d6a-a455-48d5-9152-7e234544f503",
"name": "Fetch stock fundamentals (EODHD)",
"type": "n8n-nodes-base.httpRequest",
"position": [
-352,
-24
],
"parameters": {
"url": "=https://eodhd.com/api/fundamentals/{{ $json.ticker }}.US",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "filter",
"value": "General,Highlights,Valuation,Technicals"
},
{
"name": "api_token",
"value": "YOUR_EODHD_API_KEY"
},
{
"name": "fmt",
"value": "json"
}
]
}
},
"typeVersion": 4.3
},
{
"id": "82c09546-012a-448a-be08-93fdf69069c5",
"name": "Fetch OHLC price data (EODHD)",
"type": "n8n-nodes-base.httpRequest",
"position": [
-128,
-96
],
"parameters": {
"url": "=https://eodhd.com/api/eod/{{ $json.General.Code }}.US",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "from",
"value": "2023-01-01"
},
{
"name": "to",
"value": "2025-12-30"
},
{
"name": "period",
"value": "d"
},
{
"name": "fmt",
"value": "json"
},
{
"name": "api_token",
"value": "YOUR_EODHD_API_KEY"
}
]
}
},
"typeVersion": 4.3,
"alwaysOutputData": true
},
{
"id": "68ab8847-eaab-46c1-9f68-42c92d86765c",
"name": "Generate AI stock analysis",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
768,
-24
],
"parameters": {
"text": "=Analyze the following stock data and return the output strictly following the defined JSON schema.\n\nINPUT DATA:\n{{ JSON.stringify($json) }}\n",
"options": {
"systemMessage": "You are a disciplined, quantitative and fundamental equity analyst. Your task is to evaluate a single stock using ONLY the metrics provided in the input (fundamentals, technicals, and growth_potential_score).\n\nRULES:\n- Return ONLY valid JSON. No markdown. No extra text.\n- Do NOT invent data. If a metric is missing, use null and mention the limitation in \"notes\".\n- Use probabilistic, cautious language. Never claim certainty.\n- This is NOT financial advice.\n\nOBJECTIVE:\nProduce a practical, actionable assessment: whether you would enter the trade or not, suggested entry/stop/take-profit levels, and a fundamental quality score (1\u201310) based on ratios, business quality, growth, valuation, and risk.\n\nMETRIC INTERPRETATION GUIDELINES:\n- Technicals:\n - Support / resistance: use provided levels as the primary reference.\n - RSI: >70 suggests overbought (higher short-term risk), <30 suggests oversold.\n - SMA20/50/200: trend strength improves when price > SMA200 and > SMA50.\n - volatility_ann_pct: high volatility increases risk and lowers confidence.\n- Fundamentals:\n - High PE penalizes the score unless justified by strong growth.\n - EPS and YoY revenue/earnings growth support business quality.\n - Market cap is context only (not a quality metric).\n- growth_potential_score (0\u2013100): use as a composite signal, but do not blindly follow it if it contradicts other indicators.\n\nOUTPUT SCHEMA (MANDATORY):\n{\n \"ticker\": \"string\",\n \"decision\": {\n \"would_enter\": \"YES|NO\",\n \"signal\": \"BUY|WATCH|SELL\",\n \"time_horizon\": \"short|medium|long\",\n \"confidence_0_100\": number\n },\n \"trade_plan\": {\n \"entry\": number,\n \"support\": number,\n \"resistance\": number,\n \"stop_loss\": number,\n \"take_profit\": number,\n \"rationale\": \"string\"\n },\n \"fundamental_score_1_10\": number,\n \"fundamental_assessment\": {\n \"business_quality\": \"weak|average|strong\",\n \"valuation\": \"cheap|fair|expensive|unknown\",\n \"growth\": \"low|moderate|high|unknown\",\n \"profitability\": \"low|moderate|high|unknown\"\n },\n \"positives\": [\"string\", \"string\", \"string\"],\n \"negatives\": [\"string\", \"string\", \"string\"],\n \"thesis\": [\"string\", \"string\", \"string\"],\n \"key_metrics_used\": {\n \"pe\": number,\n \"forward_pe\": number,\n \"eps\": number,\n \"revenue_growth_yoy\": number,\n \"earnings_growth_yoy\": number,\n \"rsi_14\": number,\n \"volatility_ann_pct\": number,\n \"price_vs_sma200\": \"above|below|unknown\",\n \"growth_potential_score_0_100\": number\n },\n \"notes\": [\"string\"]\n}\n\nTRADE LEVEL LOGIC:\n- support: use technical.support_30d if available; otherwise null.\n- resistance: use technical.resistance_30d if available; otherwise null.\n- entry:\n - BUY: near support (but not below it).\n - WATCH: conservative entry (near support or after confirmation).\n - SELL: entry may be null or near resistance (hypothetical short).\n- stop_loss:\n - BUY/WATCH: support * 0.97 (\u22483% below support) if support exists;\n otherwise last_close * 0.93.\n- take_profit:\n - BUY/WATCH: min(resistance * 0.99, last_close * 1.10) if resistance exists;\n otherwise last_close * 1.08.\n- If RSI > 75 or volatility_ann_pct > 40, reduce confidence and be conservative with take_profit.\n\nFUNDAMENTAL SCORE (1\u201310):\nEvaluate holistically:\n- Growth (YoY revenue/earnings, EPS quality)\n- Valuation (PE, Forward PE, PEG when available)\n- Business quality and stability\n- Risk (volatility, trend strength)\n- Use growth_potential_score as a tiebreaker\nReturn an integer from 1 to 10.\n\nINPUT:\nYou will receive a JSON object containing fields such as:\n{\n \"ticker\": \"...\",\n \"fundamentals\": {...},\n \"technical\": {...},\n \"growth_potential_score\": ...\n}\n"
},
"promptType": "define"
},
"typeVersion": 3
},
{
"id": "0194e08c-d067-40ef-bc35-fdb03e8d5097",
"name": "Compute indicators and growth score",
"type": "n8n-nodes-base.code",
"position": [
96,
-96
],
"parameters": {
"jsCode": "var arr = [];\nfor (var i = 0; i < items.length; i++) {\n arr.push(items[i].json);\n}\n\n// Sort by date ascending (recommended)\narr.sort(function(a, b) {\n return String(a.date).localeCompare(String(b.date));\n});\n\nreturn [{ json: { ohlc: arr, ohlc_len: arr.length } }];\n"
},
"typeVersion": 2
},
{
"id": "13f4df9f-2fca-40b5-9f2f-dacc837b0ad5",
"name": "Normalize OHLC data",
"type": "n8n-nodes-base.code",
"position": [
544,
-24
],
"parameters": {
"jsCode": "// n8n Code node (WAF-safe / ES5)\n// Output: 1 item with fundamentals + OHLCV metrics + growth_potential_score\n// No modern JS: no =>, no ?. , no ??, no backticks, no spread\n\nfunction isArray(x) {\n return Object.prototype.toString.call(x) === '[object Array]';\n}\nfunction toNum(x) {\n var n = Number(x);\n return (isNaN(n) || !isFinite(n)) ? null : n;\n}\nfunction safeStr(x) {\n if (x === undefined || x === null) return '';\n return String(x);\n}\nfunction mean(arr) {\n if (!arr || !arr.length) return null;\n var s = 0, c = 0;\n for (var i = 0; i < arr.length; i++) {\n if (arr[i] !== null) { s += arr[i]; c++; }\n }\n return c ? (s / c) : null;\n}\nfunction std(arr) {\n if (!arr || arr.length < 2) return null;\n var m = mean(arr);\n if (m === null) return null;\n var s = 0, c = 0;\n for (var i = 0; i < arr.length; i++) {\n if (arr[i] !== null) {\n var d = arr[i] - m;\n s += d * d;\n c++;\n }\n }\n if (c < 2) return null;\n return Math.sqrt(s / (c - 1));\n}\nfunction sma(values, period) {\n if (!values || values.length < period) return null;\n var sum = 0;\n for (var i = values.length - period; i < values.length; i++) sum += values[i];\n return sum / period;\n}\nfunction rsi(values, period) {\n if (!values || values.length < period + 1) return null;\n var gains = 0;\n var losses = 0;\n for (var i = values.length - period; i < values.length; i++) {\n var ch = values[i] - values[i - 1];\n if (ch >= 0) gains += ch;\n else losses += (-ch);\n }\n var avgGain = gains / period;\n var avgLoss = losses / period;\n if (avgLoss === 0) return 100;\n var rs = avgGain / avgLoss;\n return 100 - (100 / (1 + rs));\n}\nfunction pct(a, b) {\n if (a === null || b === null || b === 0) return null;\n return ((a / b) - 1) * 100;\n}\nfunction clamp(x, a, b) {\n return Math.max(a, Math.min(b, x));\n}\n\n// ------------------------------------------------------------\n// 1) Find fundamentals object (the one that contains General/Highlights)\n// ------------------------------------------------------------\nfunction findFundamentalsObject(allItems) {\n for (var i = 0; i < allItems.length; i++) {\n var j = allItems[i].json;\n if (j && j.General && j.Highlights) return j;\n }\n // sometimes fundamentals nested\n for (var k = 0; k < allItems.length; k++) {\n var jj = allItems[k].json;\n if (jj && jj.fundamentals && jj.fundamentals.General) return jj.fundamentals;\n }\n return null;\n}\n\n// ------------------------------------------------------------\n// 2) Collect OHLCV candles\n// Supports these cases:\n// A) items[] are candles (each item.json has date/open/high/low/close)\n// B) one item.json has ohlc array: { ohlc: [...] }\n// ------------------------------------------------------------\nfunction collectCandles(allItems) {\n // Case B: already aggregated\n for (var i = 0; i < allItems.length; i++) {\n var j = allItems[i].json;\n if (j && j.ohlc && isArray(j.ohlc) && j.ohlc.length) return j.ohlc;\n }\n\n // Case A: items are candles\n var candles = [];\n for (var k = 0; k < allItems.length; k++) {\n var c = allItems[k].json;\n if (c && c.date !== undefined && c.close !== undefined) {\n candles.push(c);\n }\n }\n if (candles.length) return candles;\n\n return null;\n}\n\n// ------------------------------------------------------------\n// 3) Extract fundamentals fields from your confirmed structure\n// ------------------------------------------------------------\nfunction extractFundamentals(f) {\n var gen = f.General || {};\n var hi = f.Highlights || {};\n var val = f.Valuation || {};\n var tech = f.Technicals || {};\n\n var out = {\n ticker: gen.Code || '',\n symbol: gen.PrimaryTicker || '',\n sector: gen.Sector || null,\n industry: gen.Industry || null,\n\n market_cap: toNum(hi.MarketCapitalization),\n pe: toNum(hi.PERatio),\n forward_pe: toNum(val.ForwardPE),\n eps: toNum(hi.EarningsShare),\n eps_ttm: toNum(hi.DilutedEpsTTM),\n peg: toNum(hi.PEGRatio),\n\n revenue_ttm: toNum(hi.RevenueTTM),\n gross_profit_ttm: toNum(hi.GrossProfitTTM),\n\n q_rev_growth_yoy: (hi.QuarterlyRevenueGrowthYOY !== undefined) ? toNum(hi.QuarterlyRevenueGrowthYOY) : null,\n q_eps_growth_yoy: (hi.QuarterlyEarningsGrowthYOY !== undefined) ? toNum(hi.QuarterlyEarningsGrowthYOY) : null,\n\n beta: toNum(tech.Beta),\n ma_50d: toNum(tech[\"50DayMA\"]),\n ma_200d: toNum(tech[\"200DayMA\"]),\n week52_high: toNum(tech[\"52WeekHigh\"]),\n week52_low: toNum(tech[\"52WeekLow\"])\n };\n\n return out;\n}\n\n// ------------------------------------------------------------\n// 4) Compute OHLCV technical metrics\n// ------------------------------------------------------------\nfunction computeTechnicals(candles) {\n // sort by date asc\n candles.sort(function(a, b) {\n return safeStr(a.date).localeCompare(safeStr(b.date));\n });\n\n var closes = [];\n var lows = [];\n var highs = [];\n var vols = [];\n var dates = [];\n\n for (var i = 0; i < candles.length; i++) {\n var c = toNum(candles[i].close);\n if (c === null) continue;\n closes.push(c);\n lows.push(toNum(candles[i].low));\n highs.push(toNum(candles[i].high));\n vols.push(toNum(candles[i].volume));\n dates.push(safeStr(candles[i].date));\n }\n\n if (closes.length < 60) {\n return { error: 'Not enough OHLC data', closes_count: closes.length };\n }\n\n var lastClose = closes[closes.length - 1];\n var lastDate = dates[dates.length - 1];\n\n // returns\n function closeAt(daysAgo) {\n var idx = closes.length - 1 - daysAgo;\n if (idx < 0) return null;\n return closes[idx];\n }\n\n var ret30 = pct(lastClose, closeAt(30));\n var ret90 = pct(lastClose, closeAt(90));\n\n // support/resistance 30\n var start30 = Math.max(0, closes.length - 30);\n var support30 = null;\n var resist30 = null;\n for (var j = start30; j < closes.length; j++) {\n var lo = lows[j];\n var hi = highs[j];\n if (lo !== null) support30 = (support30 === null) ? lo : Math.min(support30, lo);\n if (hi !== null) resist30 = (resist30 === null) ? hi : Math.max(resist30, hi);\n }\n\n // volatility (annualized %)\n var rets = [];\n for (var k = 1; k < closes.length; k++) {\n rets.push((closes[k] / closes[k - 1]) - 1);\n }\n var volDaily = std(rets);\n var volAnnPct = (volDaily === null) ? null : (volDaily * Math.sqrt(252) * 100);\n\n // RSI/SMA\n var rsi14 = rsi(closes, 14);\n var sma20 = sma(closes, 20);\n var sma50 = sma(closes, 50);\n var sma200 = sma(closes, 200);\n\n return {\n date: lastDate,\n last_close: lastClose,\n return_30d_pct: ret30,\n return_90d_pct: ret90,\n volatility_ann_pct: volAnnPct,\n rsi_14: rsi14,\n sma_20: sma20,\n sma_50: sma50,\n sma_200: sma200,\n support_30d: support30,\n resistance_30d: resist30\n };\n}\n\n// ------------------------------------------------------------\n// 5) Growth potential score (0-100) from fundamentals + technicals\n// ------------------------------------------------------------\nfunction growthScore(fund, tech) {\n var score = 50;\n\n // Growth (YOY)\n if (fund.q_rev_growth_yoy !== null) {\n // 0.18 -> 18 points max 25\n score += clamp(fund.q_rev_growth_yoy * 100, 0, 25);\n }\n if (fund.q_eps_growth_yoy !== null) {\n score += clamp(fund.q_eps_growth_yoy * 100 * 0.5, 0, 12);\n }\n\n // Valuation penalty\n if (fund.pe !== null) {\n if (fund.pe > 45) score -= 12;\n else if (fund.pe > 35) score -= 7;\n else if (fund.pe < 12) score += 5;\n }\n\n // Trend\n if (tech.sma_50 !== null && tech.last_close > tech.sma_50) score += 6;\n if (tech.sma_200 !== null && tech.last_close > tech.sma_200) score += 6;\n if (tech.sma_20 !== null && tech.last_close > tech.sma_20) score += 3;\n\n // RSI sanity\n if (tech.rsi_14 !== null) {\n if (tech.rsi_14 > 75) score -= 5;\n else if (tech.rsi_14 < 25) score -= 2;\n }\n\n // Volatility penalty\n if (tech.volatility_ann_pct !== null) {\n if (tech.volatility_ann_pct > 45) score -= 10;\n else if (tech.volatility_ann_pct > 30) score -= 5;\n }\n\n return Math.round(clamp(score, 0, 100));\n}\n\n// ------------------------------------------------------------\n// MAIN\n// ------------------------------------------------------------\nvar fObj = findFundamentalsObject(items);\nvar candles = collectCandles(items);\n\nif (!fObj) {\n return [{ json: { error: 'Fundamentals not found in items. Check merge.', hint: 'Make sure the fundamentals item with General/Highlights is included.' } }];\n}\nif (!candles) {\n return [{ json: { error: 'OHLCV candles not found in items.', hint: 'Make sure OHLC items contain date/close or aggregate as {ohlc:[...]}' } }];\n}\n\nvar fundOut = extractFundamentals(fObj);\nvar techOut = computeTechnicals(candles);\n\nif (techOut.error) {\n return [{ json: { error: techOut.error, closes_count: techOut.closes_count || 0, ticker: fundOut.ticker, symbol: fundOut.symbol } }];\n}\n\nvar gps = growthScore(fundOut, techOut);\n\n// Final payload\nreturn [{\n json: {\n ticker: fundOut.ticker,\n symbol: fundOut.symbol,\n date: techOut.date,\n fundamentals: fundOut,\n technical: techOut,\n growth_potential_score: gps\n }\n}];\n"
},
"typeVersion": 2
},
{
"id": "9a94c125-9a65-4443-b6f8-4e4b3c938e3a",
"name": "Prepare data for Google Sheets",
"type": "n8n-nodes-base.code",
"position": [
1344,
-24
],
"parameters": {
"jsCode": "var j = items[0].json;\n\nfunction joinArr(a) {\n if (!a || !a.length) return '';\n return a.join(' | ');\n}\n\nvar out = {\n ticker: j.ticker || '',\n would_enter: j.decision ? j.decision.would_enter : '',\n signal: j.decision ? j.decision.signal : '',\n time_horizon: j.decision ? j.decision.time_horizon : '',\n confidence_0_100: j.decision ? j.decision.confidence_0_100 : null,\n\n entry: j.trade_plan ? j.trade_plan.entry : null,\n support: j.trade_plan ? j.trade_plan.support : null,\n resistance: j.trade_plan ? j.trade_plan.resistance : null,\n stop_loss: j.trade_plan ? j.trade_plan.stop_loss : null,\n take_profit: j.trade_plan ? j.trade_plan.take_profit : null,\n rationale: j.trade_plan ? j.trade_plan.rationale : '',\n\n fundamental_score_1_10: j.fundamental_score_1_10 !== undefined ? j.fundamental_score_1_10 : null,\n business_quality: j.fundamental_assessment ? j.fundamental_assessment.business_quality : '',\n valuation: j.fundamental_assessment ? j.fundamental_assessment.valuation : '',\n growth: j.fundamental_assessment ? j.fundamental_assessment.growth : '',\n profitability: j.fundamental_assessment ? j.fundamental_assessment.profitability : '',\n\n positives: joinArr(j.positives),\n negatives: joinArr(j.negatives),\n thesis: joinArr(j.thesis),\n notes: joinArr(j.notes),\n\n // key metrics\n pe: j.key_metrics_used ? j.key_metrics_used.pe : null,\n forward_pe: j.key_metrics_used ? j.key_metrics_used.forward_pe : null,\n eps: j.key_metrics_used ? j.key_metrics_used.eps : null,\n revenue_growth_yoy: j.key_metrics_used ? j.key_metrics_used.revenue_growth_yoy : null,\n earnings_growth_yoy: j.key_metrics_used ? j.key_metrics_used.earnings_growth_yoy : null,\n rsi_14: j.key_metrics_used ? j.key_metrics_used.rsi_14 : null,\n volatility_ann_pct: j.key_metrics_used ? j.key_metrics_used.volatility_ann_pct : null,\n price_vs_sma200: j.key_metrics_used ? j.key_metrics_used.price_vs_sma200 : '',\n growth_potential_score_0_100: j.key_metrics_used ? j.key_metrics_used.growth_potential_score_0_100 : null\n};\n\nreturn [{ json: out }];\n"
},
"typeVersion": 2
},
{
"id": "b8a74613-dd1e-4016-9531-235547fa16d7",
"name": "Sticky Note6",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1152,
-464
],
"parameters": {
"color": 7,
"width": 1136,
"height": 816,
"content": ""
},
"typeVersion": 1
},
{
"id": "d3948bf0-a52b-4476-bae4-94ee4dac0c3a",
"name": "Sticky Note7",
"type": "n8n-nodes-base.stickyNote",
"position": [
16,
-472
],
"parameters": {
"color": 7,
"width": 1280,
"height": 816,
"content": ""
},
"typeVersion": 1
},
{
"id": "6a6233df-23d1-4a68-a3eb-4be88087a17a",
"name": "Sticky Note8",
"type": "n8n-nodes-base.stickyNote",
"position": [
1312,
-496
],
"parameters": {
"color": 7,
"width": 1504,
"height": 864,
"content": ""
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "1fc98b75-75d5-4d52-8020-038fc4e350ec",
"connections": {
"Merge": {
"main": [
[
{
"node": "Normalize OHLC data",
"type": "main",
"index": 0
}
]
]
},
"Loop Over Items": {
"main": [
[],
[
{
"node": "Fetch stock fundamentals (EODHD)",
"type": "main",
"index": 0
}
]
]
},
"OpenAI Chat Model": {
"ai_languageModel": [
[
{
"node": "Generate AI stock analysis",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Append row in sheet": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"Code in JavaScript2": {
"main": [
[
{
"node": "Prepare data for Google Sheets",
"type": "main",
"index": 0
}
]
]
},
"Get row(s) in sheet": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"Normalize OHLC data": {
"main": [
[
{
"node": "Generate AI stock analysis",
"type": "main",
"index": 0
}
]
]
},
"Generate AI stock analysis": {
"main": [
[
{
"node": "Code in JavaScript2",
"type": "main",
"index": 0
}
]
]
},
"Fetch OHLC price data (EODHD)": {
"main": [
[
{
"node": "Compute indicators and growth score",
"type": "main",
"index": 0
}
]
]
},
"Prepare data for Google Sheets": {
"main": [
[
{
"node": "Append row in sheet",
"type": "main",
"index": 0
}
]
]
},
"Fetch stock fundamentals (EODHD)": {
"main": [
[
{
"node": "Fetch OHLC price data (EODHD)",
"type": "main",
"index": 0
},
{
"node": "Merge",
"type": "main",
"index": 1
}
]
]
},
"Compute indicators and growth score": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 0
}
]
]
},
"When clicking \u2018Execute workflow\u2019": {
"main": [
[
{
"node": "Get row(s) in sheet",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow automates end-to-end stock analysis using real market data and AI: Reads a list of stock tickers from Google Sheets Fetches fundamental data (valuation, growth, profitability) and OHLCV price data from EODHD APIs Computes key technical indicators (RSI, SMA…
Source: https://n8n.io/workflows/12333/ — 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.
K&S-Media Downloadliste SQL. Uses httpRequest, agent, googleSheets, lmChatOpenAi. Event-driven trigger; 97 nodes.
🎯 Create viral TikToks, Shorts, Reels, podcasts, and ASMR videos in minutes — all on autopilot.
Generate AI viral videos with NanoBanana & VEO3, shared on socials via Blotato 2. Uses @blotato/n8n-nodes-blotato, googleSheets, lmChatOpenAi, toolThink. Event-driven trigger; 94 nodes.
Inbox Guardian. Uses gmailTrigger, lmChatOpenAi, agent, textClassifier. Event-driven trigger; 66 nodes.
> Note: This workflow uses sticky notes extensively to document each logical section of the automation. Sticky notes are mandatory and already included to explain OCR, AI parsing, folder logic, dup