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 →
{
"updatedAt": "2026-02-05T23:01:16.094Z",
"createdAt": "2026-01-08T14:32:39.044Z",
"id": "K0JZPGQRe-jTTVFEwunI4",
"name": "AI Stock Analysis Automation (EODHD + Google Sheets + AI)",
"active": false,
"isArchived": true,
"nodes": [
{
"name": "When clicking \u2018Execute workflow\u2019",
"parameters": {},
"position": [
1856,
640
],
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"id": "ff04316c-d564-4be5-81db-0d33fb6c67f8"
},
{
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"name": "Get row(s) in sheet",
"parameters": {
"documentId": {
"__rl": true,
"cachedResultName": "Portfolio_Performance",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1ksZvZR0cuQHYJ0-W0FUZUNm3rwdudlZxckLIUcgY_r8/edit?usp=drivesdk",
"mode": "list",
"value": "1ksZvZR0cuQHYJ0-W0FUZUNm3rwdudlZxckLIUcgY_r8"
},
"options": {},
"sheetName": {
"__rl": true,
"cachedResultName": "Holdings_2026",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1ksZvZR0cuQHYJ0-W0FUZUNm3rwdudlZxckLIUcgY_r8/edit#gid=231712198",
"mode": "list",
"value": 231712198
}
},
"position": [
2080,
640
],
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"id": "f95981a5-a7fd-4391-b692-b84d6f0fab3c"
},
{
"name": "Loop Over Items",
"parameters": {
"options": {}
},
"position": [
2304,
640
],
"type": "n8n-nodes-base.splitInBatches",
"typeVersion": 3,
"id": "f76740bf-68ef-4bc3-b3bd-1f9fd97e4bcd"
},
{
"alwaysOutputData": true,
"name": "HTTP Request1",
"parameters": {
"options": {},
"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": " 6962c4de96b756.96575586"
}
]
},
"sendQuery": true,
"url": "=https://eodhd.com/api/eod/{{ $json.General.Code }}.US"
},
"position": [
2752,
432
],
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"id": "844f4e29-080c-4211-8556-884148c5461a"
},
{
"name": "Merge",
"parameters": {},
"position": [
3200,
512
],
"type": "n8n-nodes-base.merge",
"typeVersion": 3.2,
"id": "fc5bbd1e-b2e6-41c6-b9e3-82d200e28320"
},
{
"name": "Code in JavaScript",
"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"
},
"position": [
3424,
512
],
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"id": "d62ea2ee-4703-41dc-9f9a-a8791a59bc4e"
},
{
"name": "Code in JavaScript1",
"parameters": {
"jsCode": "var arr = [];\nfor (var i = 0; i < items.length; i++) {\n arr.push(items[i].json);\n}\n\n// Ordena por fecha asc (opcional, pero recomendado)\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"
},
"position": [
2976,
432
],
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"id": "6da531e0-0a9a-4447-9c9a-2242b454a0eb"
},
{
"name": "AI Agent",
"parameters": {
"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",
"text": "=Analyze the following stock data and return the output strictly following the defined JSON schema.\n\nINPUT DATA:\n{{ JSON.stringify($json) }}\n"
},
"position": [
3648,
512
],
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 3,
"id": "353cbd02-4a5b-4649-9c6f-b1bb5cc72d87"
},
{
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"name": "OpenAI Chat Model",
"parameters": {
"builtInTools": {},
"model": {
"__rl": true,
"cachedResultName": "gpt-4.1-nano",
"mode": "list",
"value": "gpt-4.1-nano"
},
"options": {}
},
"position": [
3520,
960
],
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"typeVersion": 1.3,
"id": "e16a6da7-9aea-4f06-8854-8ee524be834c"
},
{
"name": "Code in JavaScript2",
"parameters": {
"jsCode": "// Parse AI output string -> JSON object\nvar raw = items[0].json.output;\n\n// A veces viene con espacios/line breaks\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"
},
"position": [
4000,
512
],
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"id": "b8a30f47-5983-482d-9894-77eaa1a3050b"
},
{
"name": "Code in JavaScript3",
"parameters": {
"jsCode": "var j = items[0].json;\n\nfunction joinArr(a) {\n if (!a || !a.length) return '';\n // Une bullets en una sola celda\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"
},
"position": [
4224,
512
],
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"id": "d17d4d61-9e55-4929-81dd-f1f2568163c8"
},
{
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"name": "Append row in sheet",
"parameters": {
"columns": {
"attemptToConvertTypes": false,
"convertFieldsToString": false,
"mappingMode": "defineBelow",
"matchingColumns": [],
"schema": [
{
"canBeUsedToMatch": true,
"defaultMatch": false,
"display": true,
"displayName": "ticker",
"id": "ticker",
"required": false,
"type": "string"
},
{
"canBeUsedToMatch": true,
"defaultMatch": false,
"display": true,
"displayName": "ENTER(YES/NO)",
"id": "ENTER(YES/NO)",
"required": false,
"type": "string"
},
{
"canBeUsedToMatch": true,
"defaultMatch": false,
"display": true,
"displayName": "ENTRY",
"id": "ENTRY",
"required": false,
"type": "string"
},
{
"canBeUsedToMatch": true,
"defaultMatch": false,
"display": true,
"displayName": "support",
"id": "support",
"required": false,
"type": "string"
},
{
"canBeUsedToMatch": true,
"defaultMatch": false,
"display": true,
"displayName": "resistance",
"id": "resistance",
"required": false,
"type": "string"
},
{
"canBeUsedToMatch": true,
"defaultMatch": false,
"display": true,
"displayName": "stop_loss",
"id": "stop_loss",
"required": false,
"type": "string"
},
{
"canBeUsedToMatch": true,
"defaultMatch": false,
"display": true,
"displayName": "take_profit",
"id": "take_profit",
"required": false,
"type": "string"
},
{
"canBeUsedToMatch": true,
"defaultMatch": false,
"display": true,
"displayName": "tecnhical_tesis",
"id": "tecnhical_tesis",
"required": false,
"type": "string"
},
{
"canBeUsedToMatch": true,
"defaultMatch": false,
"display": true,
"displayName": "fundamental_score",
"id": "fundamental_score",
"required": false,
"type": "string"
},
{
"canBeUsedToMatch": true,
"defaultMatch": false,
"display": true,
"displayName": "negative",
"id": "negative",
"required": false,
"type": "string"
},
{
"canBeUsedToMatch": true,
"defaultMatch": false,
"display": true,
"displayName": "positives",
"id": "positives",
"required": false,
"type": "string"
},
{
"canBeUsedToMatch": true,
"defaultMatch": false,
"display": true,
"displayName": "fundamental_thesis",
"id": "fundamental_thesis",
"required": false,
"type": "string"
}
],
"value": {
"ENTER(YES/NO)": "={{ $json.would_enter }}",
"ENTRY": "={{ $json.entry }}",
"fundamental_score": "={{ $json.fundamental_score_1_10 }}",
"fundamental_thesis": "={{ $json.thesis }}",
"negative": "={{ $json.negatives }}",
"positives": "={{ $json.positives }}",
"resistance": "={{ $json.resistance }}",
"stop_loss": "={{ $json.stop_loss }}",
"support": "={{ $json.support }}",
"take_profit": "={{ $json.take_profit }}",
"tecnhical_tesis": "={{ $json.rationale }}",
"ticker": "={{ $json.ticker }}"
}
},
"documentId": {
"__rl": true,
"cachedResultName": "Portfolio_Performance",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1ksZvZR0cuQHYJ0-W0FUZUNm3rwdudlZxckLIUcgY_r8/edit?usp=drivesdk",
"mode": "list",
"value": "1ksZvZR0cuQHYJ0-W0FUZUNm3rwdudlZxckLIUcgY_r8"
},
"operation": "append",
"options": {},
"sheetName": {
"__rl": true,
"cachedResultName": "Holdings_2026",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1ksZvZR0cuQHYJ0-W0FUZUNm3rwdudlZxckLIUcgY_r8/edit#gid=231712198",
"mode": "list",
"value": 231712198
}
},
"position": [
4448,
640
],
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"id": "d13eea8e-bce5-4f21-96b5-bbcb4853d366"
},
{
"name": "Sticky Note",
"parameters": {
"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\n### Key benefits\n1. **Automated & scalable** \n Analyze dozens of stocks in minutes without manual work.\n\n2. **Data-driven decisions** \n Combines fundamentals, technicals, and AI reasoning in one place.\n\n3. **Actionable outputs** \n Clear trade levels, risk notes, and investment scores\u2014ready to use.\n\n---\n\n### Who this workflow is for\n- Retail investors and swing traders \n- Data-driven investors and analysts \n- Automation builders using n8n \n- Anyone wanting AI-assisted stock analysis without writing code\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 API\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",
"height": 1872,
"width": 832
},
"position": [
896,
-160
],
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"id": "4f19f3f7-0090-4cb8-96aa-3554d6594a5e"
},
{
"name": "Sticky Note1",
"parameters": {
"content": "## INPUT\n\n\n",
"height": 336
},
"position": [
1792,
192
],
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"id": "47cd1c34-a8ef-4ec0-8b85-2b937abfce66"
},
{
"name": "Sticky Note2",
"parameters": {
"color": 7,
"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.",
"height": 224,
"width": 624
},
"position": [
3696,
192
],
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"id": "aab476af-a23a-45f6-bc51-30aa8db91706"
},
{
"name": "Sticky Note3",
"parameters": {
"color": 7,
"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",
"height": 192,
"width": 576
},
"position": [
2960,
208
],
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"id": "82922d6d-73db-4a36-bb4b-b3b311067dc2"
},
{
"name": "Sticky Note4",
"parameters": {
"color": 7,
"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.",
"height": 192,
"width": 480
},
"position": [
2160,
224
],
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"id": "907f4da5-c643-470e-9b40-1bc7518c8dda"
},
{
"name": "Sticky Note5",
"parameters": {
"content": "## OUTPUT\n",
"height": 208,
"width": 1008
},
"position": [
4416,
272
],
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"id": "525b201d-5c7d-473b-bda9-4d3b827f3b1b"
},
{
"name": "Call EODHD Financial API",
"parameters": {
"options": {
"redirect": {
"redirect": {}
}
},
"queryParameters": {
"parameters": [
{
"name": "api_token",
"value": " 6962c4de96b756.96575586"
},
{
"name": "fmt",
"value": "json"
}
]
},
"sendQuery": true,
"url": "=https://eodhd.com/api/eod/{{ $json.Symbol }}.US"
},
"position": [
2528,
512
],
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"id": "9f5046b4-6eef-4f4f-b3e8-e26d8370c082"
},
{
"credentials": {
"ollamaApi": {
"name": "<your credential>"
}
},
"name": "Ollama Chat Model",
"parameters": {
"model": "glm-4.7:cloud",
"options": {}
},
"position": [
3648,
688
],
"type": "@n8n/n8n-nodes-langchain.lmChatOllama",
"typeVersion": 1,
"id": "ec6767f9-0f51-42d4-a4b0-60ea80589e52"
}
],
"connections": {
"AI Agent": {
"main": [
[
{
"index": 0,
"node": "Code in JavaScript2",
"type": "main"
}
]
]
},
"Append row in sheet": {
"main": [
[
{
"index": 0,
"node": "Loop Over Items",
"type": "main"
}
]
]
},
"Call EODHD Financial API": {
"main": [
[
{
"index": 0,
"node": "HTTP Request1",
"type": "main"
},
{
"index": 1,
"node": "Merge",
"type": "main"
}
]
]
},
"Code in JavaScript": {
"main": [
[
{
"index": 0,
"node": "AI Agent",
"type": "main"
}
]
]
},
"Code in JavaScript1": {
"main": [
[
{
"index": 0,
"node": "Merge",
"type": "main"
}
]
]
},
"Code in JavaScript2": {
"main": [
[
{
"index": 0,
"node": "Code in JavaScript3",
"type": "main"
}
]
]
},
"Code in JavaScript3": {
"main": [
[
{
"index": 0,
"node": "Append row in sheet",
"type": "main"
}
]
]
},
"Get row(s) in sheet": {
"main": [
[
{
"index": 0,
"node": "Loop Over Items",
"type": "main"
}
]
]
},
"HTTP Request1": {
"main": [
[
{
"index": 0,
"node": "Code in JavaScript1",
"type": "main"
}
]
]
},
"Loop Over Items": {
"main": [
[],
[
{
"index": 0,
"node": "Call EODHD Financial API",
"type": "main"
}
]
]
},
"Merge": {
"main": [
[
{
"index": 0,
"node": "Code in JavaScript",
"type": "main"
}
]
]
},
"Ollama Chat Model": {
"ai_languageModel": [
[
{
"index": 0,
"node": "AI Agent",
"type": "ai_languageModel"
}
]
]
},
"OpenAI Chat Model": {
"ai_languageModel": [
[]
]
},
"When clicking \u2018Execute workflow\u2019": {
"main": [
[
{
"index": 0,
"node": "Get row(s) in sheet",
"type": "main"
}
]
]
}
},
"settings": {
"executionOrder": "v1",
"availableInMCP": false,
"callerPolicy": "workflowsFromSameOwner"
},
"staticData": null,
"meta": {
"templateCredsSetupCompleted": true
},
"versionId": "0b77eb34-2c8c-4014-ae61-df12413e0e3e",
"activeVersionId": null,
"triggerCount": 0,
"shared": [
{
"updatedAt": "2026-01-08T14:32:39.044Z",
"createdAt": "2026-01-08T14:32:39.044Z",
"role": "workflow:owner",
"workflowId": "K0JZPGQRe-jTTVFEwunI4",
"projectId": "aRJv9cLftn98cx8V"
}
],
"activeVersion": null,
"tags": []
}
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.
googleSheetsOAuth2ApiollamaApiopenAiApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
AI Stock Analysis Automation (EODHD + Google Sheets + AI). Uses googleSheets, httpRequest, agent, lmChatOpenAi. Event-driven trigger; 20 nodes.
Source: https://github.com/ATHARVISM2804/n8n_workflow_main/blob/main/workflows/ai-stock-analysis-automation-(eodhd-+-google-sheets-+-ai)-.json — 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 workflow is designed for marketers, content creators, agencies, and solo founders who want to publish long‑form posts with visuals on autopilot using n8n and AI agents.
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.
> 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