This workflow corresponds to n8n.io template #13701 — we link there as the canonical source.
This workflow follows the Googlegemini → 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": "qnglBpTv8WJXkbybqH3CX",
"meta": {
"templateId": "5369",
"templateCredsSetupCompleted": true
},
"name": "AI Institutional Stock Valuation Engine with Risk Scoring & Scenario Targets",
"tags": [
{
"id": "MrLFqPuujEL6vkPD",
"name": "Institutional-Grade Stoc",
"createdAt": "2026-02-24T19:56:14.685Z",
"updatedAt": "2026-02-24T19:56:14.685Z"
},
{
"id": "T6peYIB3ixkHZuR5",
"name": "Structured Targets. Quan",
"createdAt": "2026-02-24T19:56:06.627Z",
"updatedAt": "2026-02-24T19:56:06.627Z"
},
{
"id": "oJGbBh4g2N3cpwMd",
"name": "From Financial Data to D",
"createdAt": "2026-02-24T19:56:21.859Z",
"updatedAt": "2026-02-24T19:56:21.859Z"
},
{
"id": "ontpnv94DgmZLTrw",
"name": "Disciplined Valuation. A",
"createdAt": "2026-02-24T19:55:47.902Z",
"updatedAt": "2026-02-24T19:55:47.902Z"
},
{
"id": "xEkWy3lMAYX6lO99",
"name": "AI-Powered Equity Analys",
"createdAt": "2026-02-24T19:55:58.790Z",
"updatedAt": "2026-02-24T19:55:58.790Z"
}
],
"nodes": [
{
"id": "392f7ccd-aba3-4911-859a-556535f45eea",
"name": "loop_over_tickers",
"type": "n8n-nodes-base.splitInBatches",
"position": [
320,
1168
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "6e5b9817-b6ec-4c0a-98fd-b41340ab6230",
"name": "Read_tickers_from_Sheet",
"type": "n8n-nodes-base.googleSheets",
"position": [
96,
1168
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1fHwDfRMnMZAIhE6LIo3yofVX2hwcAnBkfE7ibyBT-z4/edit#gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1fHwDfRMnMZAIhE6LIo3yofVX2hwcAnBkfE7ibyBT-z4",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1fHwDfRMnMZAIhE6LIo3yofVX2hwcAnBkfE7ibyBT-z4/edit?usp=drivesdk",
"cachedResultName": "List of stocks"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.6
},
{
"id": "0dc93919-fb1a-46df-b316-1044f11cd791",
"name": "write_sentiment_to_sheets",
"type": "n8n-nodes-base.googleSheets",
"position": [
4752,
1440
],
"parameters": {
"columns": {
"value": {
"date": "={{ $json.date }}",
"model": "={{ $json.model }}",
"stock": "={{ $json.stock }}",
"pt_base": "={{ $json.pt_base }}",
"pt_bear": "={{ $json.pt_bear }}",
"pt_bull": "={{ $json.pt_bull }}",
"rationale": "={{ $json.rationale }}",
"confidence": "={{ $json.confidence }}"
},
"schema": [
{
"id": "stock",
"type": "string",
"display": true,
"required": false,
"displayName": "stock",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "date",
"type": "string",
"display": true,
"required": false,
"displayName": "date",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "pt_bear",
"type": "string",
"display": true,
"required": false,
"displayName": "pt_bear",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "pt_base",
"type": "string",
"display": true,
"required": false,
"displayName": "pt_base",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "pt_bull",
"type": "string",
"display": true,
"required": false,
"displayName": "pt_bull",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "confidence",
"type": "string",
"display": true,
"required": false,
"displayName": "confidence",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "rationale",
"type": "string",
"display": true,
"required": false,
"displayName": "rationale",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "model",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "model",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1fbptcVE0mBjaIZJHkJzFBTdoVJVLgQOWmJXpCx40YyE/edit#gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1fbptcVE0mBjaIZJHkJzFBTdoVJVLgQOWmJXpCx40YyE",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1fbptcVE0mBjaIZJHkJzFBTdoVJVLgQOWmJXpCx40YyE/edit?usp=drivesdk",
"cachedResultName": "Sentiments of my stocks"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.6
},
{
"id": "3cbf7b55-50ae-4d1c-8f9e-56941441ef00",
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-128,
1168
],
"parameters": {
"rule": {
"interval": [
{
"daysInterval": 3,
"triggerAtHour": 16
}
]
}
},
"typeVersion": 1.2
},
{
"id": "ce8d9b54-2026-4faa-9ee1-c261170c8426",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
240,
64
],
"parameters": {
"width": 480,
"height": 440,
"content": "# Workflow Overview \n**This workflow automates the process of analyzing the sentiment of stock market news.**\n\n- retrieves a list of stock tickers from a Google Sheet \n- fetchs recent news articles for each ticker\n- uses a 2 large language model to perform sentiment analysis on the articles\n- records the sentiment scores and rationale back into a Google Sheet."
},
"typeVersion": 1
},
{
"id": "efaee1f9-e57a-4640-a1d6-eb5bc5f341af",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-160,
576
],
"parameters": {
"color": 4,
"width": 880,
"height": 832,
"content": "# 1. Daily Trigger and Stock Ticker Retrieval\n- **Schedule Trigger:** This workflow is set to run automatically every three day at 4:00 PM (Asia/Jerusalem time). This ensures that the script runs just before the markets open and you get a daily update on the sentiment of the stocks you are tracking.\n\n- **Read_tickers_from_Sheet:** This node connects to a Google Sheet named \"Stock Sentiment\" and reads the list of stock tickers from the \"stocks\" sheet. This is the source of the stocks that the workflow will analyze. I have make every three days to avoid extra fees from Alphavintage since, and you can modify it if you need.\n\n- **loop_over_tickers:** This node takes the list of tickers from the Google Sheet and processes them one by one. This allows the workflow to perform the same set of actions for each stock ticker individually."
},
"typeVersion": 1
},
{
"id": "eaca666d-2830-4e98-bfe4-44c15d452939",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
768,
-160
],
"parameters": {
"color": 5,
"width": 2200,
"height": 1436,
"content": "# 2. Financial and News Data Retrieval & Validation\n2.1 Financial Data Fetching (Alpha Vantage APIs)\nFetch Financial Data (Alpha Vantage):\nFor each ticker, the workflow sends multiple HTTP requests to Alpha Vantage to retrieve structured financial data, including:\nCompany overview (EPS, BVPS, margins, shares, revenue).\nIncome statement (quarterly revenue and net income).\nBalance sheet (debt and cash position).\nCash flow statement (operating cash flow and capital expenditures for FCF calculation).\nCurrent stock price.\nData Normalization and Processing:\nThe responses from the APIs are processed using code nodes to:\nExtract required financial fields.\nCalculate derived metrics such as revenue growth, gross margins, total debt, cash position, and free cash flow.\nStandardize numeric values and prepare structured financial data for analysis.\nFinancial Cache Validation:\nThe workflow checks Google Sheets to determine whether financial data for the ticker already exists and whether it is still fresh (within a defined time window).\nIf the data is missing or outdated \u2192 financial APIs are called.\nIf the data is still valid \u2192 cached data is reused to avoid unnecessary API calls.\nUpdate or Insert Financial Records:\nBased on the cache result:\nNew records are inserted for first-time tickers.\nExisting records are updated when refreshed data is retrieved."
},
"typeVersion": 1
},
{
"id": "92a8b9e8-cdca-4584-938f-b478b8136867",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
2992,
464
],
"parameters": {
"color": 6,
"width": 836,
"height": 1536,
"content": "# 3. Sentiment Analysis with AI\n\n- **AI Agent & Google Gemini Chat Model/ChatGPT:** This is the core of the sentiment analysis. The \"AI Agent\" node is configured with a detailed prompt that instructs the \"Google Gemini Chat Model & ChatGPT\" to act as a stock sentiment analyzer. The prompt specifies the input format (stock symbol, price targets,confidance, rational), and the desired JSON output format. The combined text of the news articles and the current stock ticker are passed to the model.l analyze all the articles at once."
},
"typeVersion": 1
},
{
"id": "aa4c0dfb-0e02-4f74-b908-27b6b81bbef1",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
3856,
464
],
"parameters": {
"color": 2,
"width": 560,
"height": 1532,
"content": "# 4. Output Formatting and Error Handling\n\n- **format_output_as_json:** The output from the AI models is a raw string that includes a JSON object. This code node extracts the clean JSON from the string and prepares it for the next steps.\n\n- **if_format_succesful:** This conditional node checks if the previous step of formatting the AI's output into a clean JSON was successful. If there was an error, it sends the workflow back to the \"AI Agent\" to try again."
},
"typeVersion": 1
},
{
"id": "cd441252-7867-40e4-9fb9-aa1e6fd53fb9",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
4464,
464
],
"parameters": {
"color": 3,
"width": 520,
"height": 1528,
"content": "# 5. Storing the Results\n\n\n- **write_sentiment_to_sheets:** Once a valid sentiment analysis result is obtained and formatted, this node appends the data to \"Sheet1\" of the \"Stock Sentiment\" Google Sheet. It records the current date, the stock ticker, the sentiment score, and the rationale provided by the AI. After this step, the workflow loops back to process the next ticker from the initial list.\n\n\n\n\n\n\n"
},
"typeVersion": 1
},
{
"id": "7eaf394e-4043-41ce-a094-b08d28413509",
"name": "Code in JavaScript",
"type": "n8n-nodes-base.code",
"position": [
4016,
1200
],
"parameters": {
"jsCode": "function pickValid(x) {\n if (!x) return null;\n const base = Number(x.pt_base);\n const bear = Number(x.pt_bear);\n const bull = Number(x.pt_bull);\n const conf = Number(x.confidence);\n if (!x.stock) return null;\n if (!isFinite(base) || base <= 0) return null;\n if (!isFinite(bear) || bear <= 0) return null;\n if (!isFinite(bull) || bull <= 0) return null;\n if (!isFinite(conf) || conf <= 0) return null;\n return x;\n}\n\nconst byStock = new Map();\n\nfor (const it of items) {\n const j = it.json || {};\n\n // Candidate extraction (supports either chatgpt_result, gemini_result, or already flattened)\n const candidates = [];\n\n if (j.chatgpt_result && typeof j.chatgpt_result === \"object\") {\n candidates.push({ ...j.chatgpt_result, model: \"CHATGPT\" });\n }\n\n if (j.gemini_result && typeof j.gemini_result === \"object\") {\n candidates.push({ ...j.gemini_result, model: \"GEMINI\" });\n }\n\n // fallback if something arrives already flattened\n if (candidates.length === 0 && j.stock && (j.pt_base || j.pt_bear || j.pt_bull)) {\n candidates.push({ ...j, model: j.model || \"UNKNOWN\" });\n }\n\n for (const cand of candidates) {\n const stock = cand.stock || j.stock || \"\";\n const normalized = {\n stock,\n date: cand.date || null,\n pt_bear: Number(cand.pt_bear ?? NaN),\n pt_base: Number(cand.pt_base ?? NaN),\n pt_bull: Number(cand.pt_bull ?? NaN),\n f_score: (cand.f_score ?? null),\n confidence: Number(cand.confidence ?? NaN),\n rationale: cand.rationale || \"\",\n model: cand.model || \"UNKNOWN\"\n };\n\n const valid = pickValid(normalized);\n if (!valid) continue;\n\n if (!byStock.has(stock)) byStock.set(stock, []);\n byStock.get(stock).push(valid);\n }\n}\n\nconst today = new Date().toISOString().slice(0, 10);\n\nconst out = [];\nfor (const [stock, arr] of byStock.entries()) {\n // Optional: dedupe per model (keep highest confidence per model)\n const bestByModel = new Map();\n for (const r of arr) {\n const key = r.model;\n const prev = bestByModel.get(key);\n if (!prev || (r.confidence ?? 0) > (prev.confidence ?? 0)) {\n bestByModel.set(key, r);\n }\n }\n\n // Emit one row per model\n for (const r of bestByModel.values()) {\n const date = r.date || today;\n\n out.push({\n json: {\n stock,\n date,\n pt_bear: Number(r.pt_bear.toFixed(2)),\n pt_base: Number(r.pt_base.toFixed(2)),\n pt_bull: Number(r.pt_bull.toFixed(2)),\n f_score: r.f_score,\n confidence: Math.max(20, Math.min(90, Number(r.confidence.toFixed(0)))),\n rationale: r.rationale || \"No rationale provided\",\n model: r.model,\n // Use this as your Google Sheets upsert key to avoid overwriting\n row_key: `${stock}_${date}_${r.model}`,\n skip_row: false\n }\n });\n }\n}\n\nif (out.length === 0) {\n return [{\n json: {\n stock: \"\",\n date: today,\n pt_bear: 0,\n pt_base: 0,\n pt_bull: 0,\n confidence: 0,\n rationale: \"No valid analyst data\",\n model: \"ERROR\",\n row_key: `ERROR_${today}`,\n skip_row: true\n }\n }];\n}\n\nreturn out;"
},
"typeVersion": 2
},
{
"id": "4b094b92-271b-422c-9d89-b603dd199390",
"name": "Merge",
"type": "n8n-nodes-base.merge",
"position": [
3648,
1104
],
"parameters": {
"mode": "combine",
"options": {},
"joinMode": "keepEverything",
"fieldsToMatchString": "stock"
},
"typeVersion": 3.2
},
{
"id": "ea73393c-1449-43ae-8014-0b59f4b60aa1",
"name": "Message a model",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
3120,
880
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "gpt-5-mini",
"cachedResultName": "GPT-5-MINI"
},
"options": {
"textFormat": {
"textOptions": {
"type": "json_object",
"verbosity": "low"
}
}
},
"responses": {
"values": [
{
"content": "=You are a Senior Equity Analyst with 50+ years of institutional market experience.\nYou receive structured financial data, news context, and a Quality F-Score and must produce disciplined valuation targets and a risk-adjusted verdict.\nYou must think like an institutional portfolio manager: conservative, risk-first, evidence-based.\n\nINPUT FIELDS AVAILABLE:\n\nSTRUCTURED DATA INPUT (REAL VALUES \u2014 USE THESE ONLY)\n{\n\"stock\": \"{{ $json.stock }}\",\n\"current_price\": {{ $json.current_price }},\n\"eps_current\": {{ $json.eps_current }},\n\"bvps_current\": {{ $json.bvps_current }},\n\"revenue_last_4q\": {{ $json.revenue_last_4q }},\n\"net_income_last_4q\": {{ $json.net_income_last_4q }},\n\"gross_margin_last_4q\": {{ $json.gross_margin_last_4q }},\n\"total_debt_last_4q\": {{ $json.total_debt_last_4q }},\n\"net_debt_latest\": {{ $json.net_debt_latest }},\n\"net_cash_flag\": {{ $json.net_cash_flag }},\n\"operating_margin_ttm\": {{ $json.operating_margin_ttm }},\n\"fcf_ttm\": {{ $json.fcf_ttm }},\n\"revenue_growth_yoy\": {{ $json.revenue_growth_yoy }},\n\"f_score\": {{ $json.f_score }},\n\"f_score_data_ok\": {{ $json.f_score_data_ok }}\n}\n\nRules:\n\nUse ONLY these values.\n\nIf any field is null \u2192 treat as missing.\n\nNever assume missing data.\n\nNever assume share splits, ADR ratios, or alternative listings.\n\nIMPORTANT \u2014 NEWS SOURCE PRIORITY:\n\nSeeking Alpha articles (highest credibility signal)\n\nOther news_items\n\nMarket sentiment\n\nPrefer Seeking Alpha for thesis framing, but verify with financial data.\nIf conflict exists, flag uncertainty.\n\nSEEKING ALPHA ARTICLES (Recent 80h window):\n{{ $json.seekingAlphaNewsText }}\n\nANALYSIS TASKS\n\nFundamental Trends\n\nEvaluate revenue_last_4q and net_income_last_4q.\n\nEvaluate debt using total_debt_last_4q and net_debt_latest.\n\nEvaluate margin stability using gross_margin_last_4q and operating_margin_ttm.\n\nIf fcf_ttm is null \u2192 state \"FCF unavailable due to missing data.\"\n\nBusiness & Moat\nClassify moat: Brand, Network, Cost, Switching Costs, or IP.\n\nPricing Power\nJudge using margin stability and operating_margin_ttm.\n\nVALUATION FRAMEWORK (CRITICAL SECTION)\n\nA) IMPLIED P/E SANITY CHECK (MANDATORY)\n\nIf eps_current > 0 AND current_price > 0:\n\nimplied_pe = current_price / eps_current\nElse:\n\nimplied_pe = null\n\nEPS VALIDITY RULE:\nIf implied_pe is not null AND implied_pe > 60:\n\nTreat EPS as NOT economically meaningful (growth/transition phase).\n\nEPS multiple valuation is FORBIDDEN.\n\nCompany must be classified as growth_transition.\n\nB) PHASE CLASSIFICATION (MANDATORY \u2014 must pick one)\n\n\"mature_profitable\": eps_current > 0 AND (implied_pe is null OR implied_pe \u2264 60)\n\n\"growth_transition\": eps_current \u2264 0 OR implied_pe > 60\n\nC) Graham Number (secondary only)\n\nsqrt(22.5 \u00d7 eps_current \u00d7 bvps_current)\n\nUse ONLY if:\n\neps_current > 0\n\nbvps_current > 0\n\nphase = mature_profitable\n\nDo NOT use Graham for growth_transition stocks.\n\nD) BASE TARGET CONSTRUCTION (pt_base)\n\nPrimary valuation logic depends on phase.\n\nIF phase = mature_profitable:\n\nIf EPS positive:\npt_base = eps_current \u00d7 multiple:\n\n25\u00d7 if revenue_growth_yoy > 20%\n\n18\u00d7 if revenue_growth_yoy 5\u201320%\n\n12\u00d7 otherwise\n\nIf Graham usable:\npt_base = average(pt_base, Graham)\n\nIF phase = growth_transition:\nGrowth-based anchor only (EPS multiples forbidden):\n\nLet g = revenue_growth_yoy\n\nIf g is null \u2192 pt_base = current_price\n\nIf g > 60 \u2192 pt_base = current_price \u00d7 2.0\n\nIf g > 30 \u2192 pt_base = current_price \u00d7 1.6\n\nIf g > 15 \u2192 pt_base = current_price \u00d7 1.3\n\nOtherwise \u2192 pt_base = current_price\n\nE) FINAL SAFETY RULES\n\npt_base must never be zero.\n\nIf pt_base cannot be determined \u2192 pt_base = current_price.\n\npt_base must not exceed current_price \u00d7 5 unless revenue_growth_yoy > 60.\n\nRound pt_base to 2 decimals.\n\nNEWS & THESIS IMPACT ANALYSIS (REQUIRED)\n\nAnalyze Seeking Alpha + news_items.\nExtract:\n\nregulatory risks\n\nlegal risks\n\nearnings/guidance changes\n\ncompetitive pressure\n\nthesis change signals\n\nDetermine if thesis strengthened or weakened.\n\nANTI-THESIS (RISK ADJUSTMENT)\n\nIdentify TWO biggest risks using priority:\n\nSeeking Alpha risks\n\nFinancial deterioration\n\nGeneral news\n\nF-Score Handling:\nIf f_score_data_ok is false \u2192 reduce confidence by 10.\nIf f_score \u2264 3 \u2192 increase risk discount by +10%.\nIf f_score \u2265 7 \u2192 increase confidence by +5 (max 90).\n\nBEAR / BULL BANDS\n\nDiscount selection:\n\n15% \u2192 strong fundamentals\n\n25% \u2192 moderate risk\n\n35% \u2192 weak fundamentals or uncertainty\n\nPremium selection:\n\n15% \u2192 slow growth\n\n25% \u2192 moderate growth\n\n40% \u2192 high growth (>20% revenue growth)\n\nCompute:\npt_bear = pt_base \u00d7 (1 \u2212 discount)\npt_bull = pt_base \u00d7 (1 + premium)\n\nRound both to 2 decimals.\n\nACTION VERDICT LOGIC\n\nBUY:\n\npt_base \u2265 20% upside\n\nand (f_score \u2265 5 OR fundamentals strong)\n\nSELL:\n\npt_base \u2264 -15% downside\n\nor (f_score_data_ok true AND f_score \u2264 2)\n\nOtherwise HOLD.\n\nCONFIDENCE SCORING\n\nStart at 60.\n\n+10 if revenue_growth_yoy > 20%.\n+10 if strong balance sheet (net_cash_flag true or low debt).\n+5 if f_score \u2265 7.\n\n-15 if declining margins or losses.\n-15 if high leverage.\n-20 if major regulatory/legal risk.\n-10 if F-score unavailable.\n\nClamp between 20 and 90.\n\nOUTPUT FORMAT \u2014 STRICT JSON ONLY\n\nReturn ONLY raw JSON object. No markdown. No commentary.\n\n{\n\"stock\": \"{{ $json.stock }}\",\n\"date\": \"{{ $now.format('yyyy-MM-dd') }}\",\n\"pt_bear\": (number),\n\"pt_base\": (number),\n\"pt_bull\": (number),\n\"f_score\": {{ $json.f_score }},\n\"confidence\": (20-90),\n\"rationale\": \"A one-sentence summary starting with BUY/HOLD/SELL, followed by the moat, primary risk, and pricing power level.\"\n}"
}
]
},
"builtInTools": {}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.1
},
{
"id": "b6d3f2f7-a27a-42cf-8942-5e0cd21386a8",
"name": "XML",
"type": "n8n-nodes-base.xml",
"position": [
1744,
1680
],
"parameters": {
"options": {},
"dataPropertyName": "sa_xml"
},
"typeVersion": 1
},
{
"id": "3a1f9abb-a8bd-45c6-ae5d-f0f3e86a22cf",
"name": "Merge1",
"type": "n8n-nodes-base.merge",
"position": [
2816,
1088
],
"parameters": {
"mode": "combine",
"options": {},
"joinMode": "keepEverything",
"fieldsToMatchString": "stock"
},
"typeVersion": 3.2
},
{
"id": "f1c73d00-f4de-4f2f-a229-6998da7baf74",
"name": "If",
"type": "n8n-nodes-base.if",
"position": [
4224,
1312
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "8659bdf4-3891-4cf4-9187-ea800a3810f8",
"operator": {
"type": "boolean",
"operation": "false",
"singleValue": true
},
"leftValue": "={{$json.skip_row}}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.3
},
{
"id": "be94ef8b-ea23-4023-835c-8705558e589a",
"name": "If1",
"type": "n8n-nodes-base.if",
"position": [
1312,
1680
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "bf9993a5-39a5-4344-a1a6-5ba38e67e47c",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "sa_xml",
"rightValue": ""
}
]
}
},
"typeVersion": 2.3
},
{
"id": "1a5039a2-9424-44d5-90b6-0b6c496a3a8c",
"name": "Edit Fields2",
"type": "n8n-nodes-base.set",
"position": [
1936,
1680
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "44ff26a3-5901-4e3b-9b15-9844a2d207b5",
"name": "stock",
"type": "string",
"value": "={{ $('Clean the news').item.json.stock }}"
},
{
"id": "fca99ba0-11fd-40a3-acf7-ea25579a653e",
"name": "rss",
"type": "object",
"value": "={{ $json.rss }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "85c8e4cc-a8fb-44b4-9da5-24e1c4e074cf",
"name": "Merge2",
"type": "n8n-nodes-base.merge",
"position": [
1808,
320
],
"parameters": {
"mode": "combine",
"options": {},
"joinMode": "keepEverything",
"fieldsToMatchString": "stock"
},
"typeVersion": 3.2
},
{
"id": "0a98c267-b2d2-4a23-ad2c-29ee26a1f873",
"name": "Is Cache Valid?",
"type": "n8n-nodes-base.if",
"position": [
944,
640
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "8579a7cf-3060-4026-b94a-5e5f370c63ae",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{$json.shouldFetch}}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.3
},
{
"id": "35fcac15-4883-4a6c-a134-6ffab038c940",
"name": "Cache Lookup",
"type": "n8n-nodes-base.googleSheets",
"maxTries": 3,
"position": [
576,
1056
],
"parameters": {
"options": {
"returnFirstMatch": false
},
"filtersUI": {
"values": [
{
"lookupValue": "={{ $json.stock }}",
"lookupColumn": "stock"
}
]
},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1IQeSDT8cqPKmpJgDYIl14fPM_lON_cW1W73KJNn65z8/edit#gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1IQeSDT8cqPKmpJgDYIl14fPM_lON_cW1W73KJNn65z8",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1IQeSDT8cqPKmpJgDYIl14fPM_lON_cW1W73KJNn65z8/edit?usp=drivesdk",
"cachedResultName": "Financial_Data_Cache"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"retryOnFail": false,
"typeVersion": 4.7,
"alwaysOutputData": true
},
{
"id": "b5e7e0a9-3dd5-4376-ba58-685db5dc98b6",
"name": "alphavantage - Balance Sheet",
"type": "n8n-nodes-base.httpRequest",
"position": [
1232,
304
],
"parameters": {
"url": "=https://www.alphavantage.co/query?function=BALANCE_SHEET&symbol={{ $('loop_over_tickers').item.json.stock }}&apikey=NV2BZ902M2RIVKPC",
"options": {
"response": {
"response": {
"responseFormat": "json"
}
}
}
},
"typeVersion": 4.4
},
{
"id": "b189892c-35da-478a-ad6f-23b7f950d1ed",
"name": "alphavantage - Profile",
"type": "n8n-nodes-base.httpRequest",
"position": [
1232,
512
],
"parameters": {
"url": "=https://www.alphavantage.co/query?function=OVERVIEW&symbol={{ $('loop_over_tickers').item.json.stock }}&apikey=NV2BZ902M2RIVKPC",
"options": {
"response": {
"response": {
"responseFormat": "json"
}
}
}
},
"typeVersion": 4.4
},
{
"id": "40deac93-692c-4df4-bc28-40af485ddd14",
"name": "alphavantage - Income Statement",
"type": "n8n-nodes-base.httpRequest",
"position": [
1232,
688
],
"parameters": {
"url": "=https://www.alphavantage.co/query?function=INCOME_STATEMENT&symbol={{ $('loop_over_tickers').item.json.stock }}&apikey=NV2BZ902M2RIVKPC",
"options": {
"response": {
"response": {
"responseFormat": "json"
}
}
}
},
"typeVersion": 4.4
},
{
"id": "0203cdb9-24dd-403e-8928-34b482fb68ac",
"name": "Merge3",
"type": "n8n-nodes-base.merge",
"position": [
1648,
592
],
"parameters": {
"mode": "combine",
"options": {},
"joinMode": "keepEverything",
"fieldsToMatchString": "stock"
},
"typeVersion": 3.2
},
{
"id": "3d012dc2-9ab6-4ae4-9130-6154a185a011",
"name": "Get row(s) in sheet",
"type": "n8n-nodes-base.googleSheets",
"position": [
1200,
1088
],
"parameters": {
"options": {},
"filtersUI": {
"values": [
{
"lookupValue": "={{ $('loop_over_tickers').item.json.stock }}",
"lookupColumn": "stock"
}
]
},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1IQeSDT8cqPKmpJgDYIl14fPM_lON_cW1W73KJNn65z8/edit#gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1IQeSDT8cqPKmpJgDYIl14fPM_lON_cW1W73KJNn65z8",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1IQeSDT8cqPKmpJgDYIl14fPM_lON_cW1W73KJNn65z8/edit?usp=drivesdk",
"cachedResultName": "Financial_Data_Cache"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "5c318795-5695-41ed-a8f7-89dc844cb9e2",
"name": "If2",
"type": "n8n-nodes-base.if",
"position": [
2336,
336
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "5ac312fa-a607-405d-9ab0-6993e2f45b19",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{$json.cacheHit}}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.3
},
{
"id": "ca7f63bd-a588-4ea5-97dd-b52dd7b33190",
"name": "Update row in sheet",
"type": "n8n-nodes-base.googleSheets",
"position": [
2400,
624
],
"parameters": {
"columns": {
"value": {
"stock": "={{ $json.stock }}",
"f_score": "={{ $('Clean Read Financial').item.json.f_score }}",
"fcf_ttm": "={{ $('Clean Read Financial').item.json.fcf_ttm }}",
"row_number": 0,
"eps_current": "={{ $('Clean Read Financial').item.json.eps_current }}",
"revenue_ttm": "={{ $('Clean Read Financial').item.json.revenue_ttm }}",
"bvps_current": "={{ $('Clean Read Financial').item.json.bvps_current }}",
"cash_last_4q": "={{ $('Clean Read Financial').item.json.cash_last_4q }}",
"last_updated": "={{ new Date().toISOString().split(\"T\")[0] }}",
"current_price": "={{ $('Clean Read Financial').item.json.current_price }}",
"net_income_ttm": "={{ $('Clean Read Financial').item.json.net_income_ttm }}",
"total_debt_ttm": "={{ $('Clean Read Financial').item.json.total_debt_ttm }}",
"f_score_data_ok": "={{ $('Clean Read Financial').item.json.f_score_data_ok }}",
"revenue_last_4q": "={{ $('Clean Read Financial').item.json.revenue_last_4q }}",
"gross_margin_ttm": "={{ $('Clean Read Financial').item.json.gross_margin_ttm }}",
"net_income_last_4q": "={{ $('Clean Read Financial').item.json.net_income_last_4q }}",
"revenue_growth_yoy": "={{ $('Clean Read Financial').item.json.revenue_growth_yoy }}",
"shares_outstanding": "={{ $('Clean Read Financial').item.json.shares_outstanding }}",
"total_debt_last_4q": "={{ $('Clean Read Financial').item.json.total_debt_last_4q }}",
"gross_margin_last_4q": "={{ $('Clean Read Financial').item.json.gross_margin_last_4q }}",
"operating_margin_ttm": "={{ $('Clean Read Financial').item.json.operating_margin_ttm }}"
},
"schema": [
{
"id": "stock",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "stock",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "cache_key",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "cache_key",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "data_source",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "data_source",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "last_updated",
"type": "string",
"display": true,
"required": false,
"displayName": "last_updated",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "eps_current",
"type": "string",
"display": true,
"required": false,
"displayName": "eps_current",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "bvps_current",
"type": "string",
"display": true,
"required": false,
"displayName": "bvps_current",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "shares_outstanding",
"type": "string",
"display": true,
"required": false,
"displayName": "shares_outstanding",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "revenue_ttm",
"type": "string",
"display": true,
"required": false,
"displayName": "revenue_ttm",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "total_debt_ttm",
"type": "string",
"display": true,
"required": false,
"displayName": "total_debt_ttm",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "gross_margin_ttm",
"type": "string",
"display": true,
"required": false,
"displayName": "gross_margin_ttm",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "operating_margin_ttm",
"type": "string",
"display": true,
"required": false,
"displayName": "operating_margin_ttm",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "fcf_ttm",
"type": "string",
"display": true,
"required": false,
"displayName": "fcf_ttm",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "revenue_last_4q",
"type": "string",
"display": true,
"required": false,
"displayName": "revenue_last_4q",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "net_income_last_4q",
"type": "string",
"display": true,
"required": false,
"displayName": "net_income_last_4q",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "gross_margin_last_4q",
"type": "string",
"display": true,
"required": false,
"displayName": "gross_margin_last_4q",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "total_debt_last_4q",
"type": "string",
"display": true,
"required": false,
"displayName": "total_debt_last_4q",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "cash_last_4q",
"type": "string",
"display": true,
"required": false,
"displayName": "cash_last_4q",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "net_income_ttm",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "net_income_ttm",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "current_price",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "current_price",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "revenue_growth_yoy",
"type": "string",
"display": true,
"required": false,
"displayName": "revenue_growth_yoy",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "f_score",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "f_score",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "f_score_data_ok",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "f_score_data_ok",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "row_number",
"type": "number",
"display": true,
"removed": false,
"readOnly": true,
"required": false,
"displayName": "row_number",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"stock"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "update",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1IQeSDT8cqPKmpJgDYIl14fPM_lON_cW1W73KJNn65z8/edit#gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1IQeSDT8cqPKmpJgDYIl14fPM_lON_cW1W73KJNn65z8",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1IQeSDT8cqPKmpJgDYIl14fPM_lON_cW1W73KJNn65z8/edit?usp=drivesdk",
"cachedResultName": "Financial_Data_Cache"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "318f0afb-8875-4234-8477-075e21a2d1a5",
"name": "Insert Row",
"type": "n8n-nodes-base.googleSheets",
"position": [
2400,
848
],
"parameters": {
"columns": {
"value": {
"stock": "={{ $json.stock }}",
"f_score": "={{ $json.f_score }}",
"fcf_ttm": "={{ $json.fcf_ttm }}",
"eps_current": "={{ $json.eps_current }}",
"revenue_ttm": "={{ $json.revenue_ttm }}",
"bvps_current": "={{ $json.bvps_current }}",
"cash_last_4q": "={{ $json.cash_last_4q }}",
"last_updated": "={{ new Date().toISOString().split(\"T\")[0] }}",
"current_price": "={{ $json.current_price }}",
"net_income_ttm": "={{ $json.net_income_ttm }}",
"total_debt_ttm": "={{ $json.total_debt_ttm }}",
"f_score_data_ok": "={{ $json.f_score_data_ok }}",
"revenue_last_4q": "={{ $json.revenue_last_4q }}",
"gross_margin_ttm": "={{ $json.gross_margin_ttm }}",
"net_income_last_4q": "={{ $json.net_income_last_4q }}",
"revenue_growth_yoy": "={{ $json.revenue_growth_yoy }}",
"shares_outstanding": "={{ $json.shares_outstanding }}",
"total_debt_last_4q": "={{ $json.total_debt_last_4q }}",
"gross_margin_last_4q": "={{ $json.gross_margin_last_4q }}",
"operating_margin_ttm": "={{ $json.operating_margin_ttm }}"
},
"schema": [
{
"id": "stock",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "stock",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "cache_key",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "cache_key",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "data_source",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "data_source",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "last_updated",
"type": "string",
"display": true,
"required": false,
"displayName": "last_updated",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "eps_current",
"type": "string",
"display": true,
"required": false,
"displayName": "eps_current",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "bvps_current",
"type": "string",
"display": true,
"required": false,
"displayName": "bvps_current",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "shares_outstanding",
"type": "string",
"display": true,
"required": false,
"displayName": "shares_outstanding",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "revenue_ttm",
"type": "string",
"display": true,
"required": false,
"displayName": "revenue_ttm",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "total_debt_ttm",
"type": "string",
"display": true,
"required": false,
"displayName": "total_debt_ttm",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "gross_margin_ttm",
"type": "string",
"display": true,
"required": false,
"displayName": "gross_margin_ttm",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "operating_margin_ttm",
"type": "string",
"display": true,
"required": false,
"displayName": "operating_margin_ttm",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "fcf_ttm",
"type": "string",
"display": true,
"required": false,
"displayName": "fcf_ttm",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "revenue_last_4q",
"type": "string",
"display": true,
"required": false,
"displayName": "revenue_last_4q",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "net_income_last_4q",
"type": "string",
"display": true,
"required": false,
"displayName": "net_income_last_4q",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "gross_margin_last_4q",
"type": "string",
"display": true,
"required": false,
"displayName": "gross_margin_last_4q",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "total_debt_last_4q",
"type": "string",
"display": true,
"required": false,
"displayName": "total_debt_last_4q",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "cash_last_4q",
"type": "string",
"display": true,
"required": false,
"displayName": "cash_last_4q",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "net_income_ttm",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "net_income_ttm",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "current_price",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "current_price",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "revenue_growth_yoy",
"type": "string",
"display": true,
"required": false,
"displayName": "revenue_growth_yoy",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "f_score",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "f_score",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "f_score_data_ok",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "f_score_data_ok",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"stock"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1IQeSDT8cqPKmpJgDYIl14fPM_lON_cW1W73KJNn65z8/edit#gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1IQeSDT8cqPKmpJgDYIl14fPM_lON_cW1W73KJNn65z8",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1IQeSDT8cqPKmpJgDYIl14fPM_lON_cW1W73KJNn65z8/edit?usp=drivesdk",
"cachedResultName": "Financial_Data_Cache"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "023ff7ab-93e2-4339-a245-69696f26ea1d",
"name": "alphavantage - CashFlow",
"type": "n8n-nodes-base.httpRequest",
"position": [
1216,
864
],
"parameters": {
"url": "=https://www.alphavantage.co/query?function=CASH_FLOW&symbol={{ $('loop_over_tickers').item.json.stock }}&apikey=NV2BZ902M2RIVKPC",
"options": {
"response": {
"response": {
"responseFormat": "json"
}
}
}
},
"typeVersion": 4.4
},
{
"id": "c1ffc9d2-401b-4773-aa1c-846b5c245124",
"name": "Merge4",
"type": "n8n-nodes-base.merge",
"position": [
1984,
688
],
"parameters": {
"mode": "combine",
"options": {},
"fieldsToMatchString": "stock"
},
"typeVersion": 3.2
},
{
"id": "6da55c24-c5b0-4376-8d5c-be1f464092e9",
"name": "Check the cache",
"type": "n8n-nodes-base.code",
"position": [
800,
1024
],
"parameters": {
"jsCode": "const FRESH_DAYS = 7;\nconst now = Date.now();\n\nconst row = items?.[0]?.json ?? null;\n\nfunction parseDateMs(v) {\n if (!v) return null;\n const t = Date.parse(v);\n return Number.isFinite(t) ? t : null;\n}\n\nconst cacheHit = !!(row && row.stock);\n\nlet shouldFetch = false;\nlet reason = '';\nlet ageDays = null;\n\nif (!cacheHit) {\n shouldFetch = true;\n reason = 'cache_miss';\n} else {\n const updatedMs = parseDateMs(row.last_updated);\n\n if (!updatedMs) {\n shouldFetch = true;\n reason = 'missing_last_updated';\n } else {\n ageDays = (now - updatedMs) / 86400000;\n shouldFetch = ageDays >= FRESH_DAYS;\n reason = shouldFetch ? `stale_${ageDays.toFixed(2)}d` : `fresh_${ageDays.toFixed(2)}d`;\n }\n}\n\nreturn [{\n json: {\n stock: row?.stock || $json.stock,\n cacheHit, // \u2705 this is the one you\u2019ll use for update vs append\n shouldFetch, // \u2705 this is only for API call decision\n reason,\n ageDays,\n cacheRow: row || null,\n nowISO: new Date(now).toISOString(),\n }\n}];\n"
},
"typeVersion": 2
},
{
"id": "0a4cc04c-137e-46b4-84d0-fbf78719cd98",
"name": "Seekingalpha Articles",
"type": "n8n-nodes-base.httpRequest",
"position": [
896,
1664
],
"parameters": {
"url": "=https://seekingalpha.com/api/sa/combined/{{ $json.stock }}.xml",
"options": {
"response": {
"response": {
"responseFormat": "text",
"outputPropertyName": "sa_xml"
}
}
}
},
"typeVersion": 4.4
},
{
"id": "4f7ab138-6aa4-4d09-8811-ebe715aff036",
"name": "Clean balance sheet",
"type": "n8n-nodes-base.code",
"position": [
1440,
304
],
"parameters": {
"jsCode": "return {\n json: {\n stock: $json.stock || $json.Symbol || $json.symbol || $input.item.json.stock || \"UNKNOWN\",\n balance_data: $json\n }\n};"
},
"typeVersion": 2
},
{
"id": "9f07de28-b72c-48ca-b2ed-acc8a8166697",
"name": "Clean Profile",
"type": "n8n-nodes-base.code",
"position": [
1440,
496
],
"parameters": {
"jsCode": "return {\n json: {\n stock: $json.stock || $json.Symbol || $json.symbol || $input.item.json.stock || \"UNKNOWN\",\n overview_data: $json\n }\n};"
},
"typeVersion": 2
},
{
"id": "a372771f-529b-44aa-b1cc-7e42940d4834",
"name": "Clean Income statement",
"type": "n8n-nodes-base.code",
"position": [
1440,
688
],
"parameters": {
"jsCode": "const inJson = $json || {};\nconst stock =\n inJson.stock ||\n inJson.Symbol ||\n inJson.symbol ||\n (items && items[0] && items[0].json && items[0].json.stock) ||\n \"UNKNOWN\";\n\nreturn [\n {\n json: {\n stock: stock,\n income_data: inJson\n }\n }\n];"
},
"typeVersion": 2
},
{
"id": "81eef3bf-3089-4494-982a-f5aaf756948d",
"name": "Clean Ccashflow",
"type": "n8n-nodes-base.code",
"position": [
1440,
848
],
"parameters": {
"jsCode": "const inJson = $json || {};\nconst stock =\n inJson.stock ||\n inJson.Symbol ||\n inJson.symbol ||\n (items && items[0] && items[0].json && items[0].json.stock) ||\n \"UNKNOWN\";\n\nreturn [\n {\n json: {\n stock: stock,\n cashflow_data: inJson\n }\n }\n];"
},
"typeVersion": 2
},
{
"id": "5fe3436c-a83a-45cc-a535-85e3628c52d5",
"name": "Clean Read Financial",
"type": "n8n-nodes-base.code",
"position": [
2144,
688
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "// Helper function to safely get numbers\nfunction num(value) {\n if (value === null || value === undefined || value === 'None' || value === '') return null;\n const n = Number(String(value).replace(/[,$%]/g, ''));\n return isFinite(n) ? n : null;\n}\n\n// Get data from the API responses\nconst overview = $json.overview_data || {};\nconst income = $json.income_data || {};\nconst balance = $json.balance_data || {};\nconst cashflow = $json.cashflow_data || {}; // optional (only if you add CASH_FLOW endpoint)\nconst current_price = num($json.current_price); // null if missing\n\n\n// Extract reports\nconst quarterlyReports = income.quarterlyReports || [];\nconst balanceQuarterly = balance.quarterlyReports || [];\n\n// Revenue last 4 quarters\nconst revenue_last_4q = quarterlyReports.slice(0, 4).map(q => num(q.totalRevenue));\n\n// Net income last 4 quarters\nconst net_income_last_4q = quarterlyReports.slice(0, 4).map(q => num(q.netIncome));\n\n// Gross margin last 4 quarters (calculate from revenue and cost)\nconst gross_margin_last_4q = quarterlyReports.slice(0, 4).map(q => {\n const rev = num(q.totalRevenue);\n const cogs = num(q.costOfRevenue);\n if (rev !== null && cogs !== null && rev > 0) {\n return Number((((rev - cogs) / rev) * 100).toFixed(2));\n }\n return null;\n});\n\n// Total debt last 4 quarters\nconst total_debt_last_4q = balanceQuarterly.slice(0, 4).map(q => {\n const shortTerm = num(q.shortTermDebt) || 0;\n const longTerm = num(q.longTermDebt) || 0;\n const total = shortTerm + longTerm;\n return total > 0 ? total : null;\n});\n\n// Cash last 4 quarters\nconst cash_last_4q = balanceQuarterly\n .slice(0, 4)\n .map(q => num(q.cashAndCashEquivalentsAtCarryingValue));\n\n\nconst debt_latest =\n total_debt_last_4q && total_debt_last_4q.length\n ? total_debt_last_4q[0]\n : null;\n\nconst cash_latest =\n cash_last_4q && cash_last_4q.length\n ? cash_last_4q[0]\n : null;\n\nlet net_debt_latest = null;\nlet net_cash_flag = null;\n\nif (debt_latest !== null && cash_latest !== null) {\n net_debt_latest = debt_latest - cash_latest;\n net_cash_flag = net_debt_latest < 0;\n}\n// Revenue growth YoY (quarter 0 vs quarter 3)\nlet revenue_growth_yoy = null;\nif (revenue_last_4q.length >= 4 && revenue_last_4q[0] !== null && revenue_last_4q[3] !== null && revenue_last_4q[3] !== 0) {\n revenue_growth_yoy = Number((((revenue_last_4q[0] - revenue_last_4q[3]) / revenue_last_4q[3]) * 100).toFixed(2));\n}\n\n// ---- FIX 1: total_debt_ttm should come from BALANCE sheet (latest quarter) ----\nconst latestBQ = balanceQuarterly && balanceQuarterly.length ? balanceQuarterly[0] : null;\n\nlet total_debt_ttm = null;\nif (latestBQ) {\n const shortTerm = num(latestBQ.shortTermDebt) || 0;\n const longTerm = num(latestBQ.longTermDebt) || 0;\n const total = shortTerm + lon
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.
googlePalmApigoogleSheetsOAuth2ApiopenAiApitelegramApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
A professional AI equity analysis automation built on n8n that transforms structured financial data and real-time news into disciplined, risk-adjusted price targets and actionable BUY/HOLD/SELL signals — delivered through automation channels like Telegram or dashboards.
Source: https://n8n.io/workflows/13701/ — 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.
AI Institutional Stock Valuation Engine with Risk Scoring & Scenario Targets
Overview This is a production-grade, fully automated stock analysis system built entirely in n8n. It combines institutional-level financial analysis, dual AI model consensus, and a self-improving back
Takes a product image from Google Sheets, adds frozen effect with Gemini, generates ASMR video with Veo3, writes captions with GPT-4o, and posts to 4 platforms automatically. Schedule trigger picks fi
Automatically captures a screenshot of a tech news homepage, extracts headlines into structured JSON, logs them in Google Sheets, and posts a daily trend report (7–10 bullet points) to Telegram at 07:
Beydigital Media – Lead Generation & AI Email Automation. Uses httpRequest, openAi, gmail, googleSheets. Scheduled trigger; 18 nodes.