AI Investment Research Platform - Structured Report API
Shared 6/20/2026
0 views
Visual Workflow
JSON Code
{
"id": "TI26wAu9DVfwV0rO",
"meta": {
"instanceId": "5e3f231be1a494a391b67b58aa8e5e089419f72936c3ceb0723144641a5d7173",
"templateCredsSetupCompleted": true
},
"name": "AI Investment Research Platform - Structured Report API",
"tags": [
{
"id": "GrfYwmU4Jfcxt0qe",
"name": "ai-agent",
"createdAt": "2026-06-17T12:43:07.098Z",
"updatedAt": "2026-06-17T12:43:07.098Z"
},
{
"id": "wqtkxiQXxQ3sUdY9",
"name": "investment-research",
"createdAt": "2026-06-17T12:43:07.097Z",
"updatedAt": "2026-06-17T12:43:07.097Z"
}
],
"nodes": [
{
"id": "f68d5ff4-9b71-4a4d-8a66-10138440c001",
"name": "Docs - Workflow Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
-880,
-368
],
"parameters": {
"color": 7,
"width": 460,
"height": 280,
"content": "## Investment Research Report API\nReceives symbols by webhook, validates input, fetches market and news data, runs two isolated AI agents, aggregates a sorted report, and returns JSON.\n\nEnvironment variables:\n- TWELVE_DATA_API_KEY\n- NEWS_API_KEY\n\nCredentials:\n- Configure both OpenAI Chat Model nodes with an n8n OpenAI credential backed by OPENAI_API_KEY."
},
"typeVersion": 1
},
{
"id": "f68d5ff4-9b71-4a4d-8a66-10138440c002",
"name": "Docs - Delivery Extension Point",
"type": "n8n-nodes-base.stickyNote",
"position": [
3824,
-336
],
"parameters": {
"color": 5,
"width": 420,
"height": 180,
"content": "## Extension Points\nThe final report passes through `Code - Delivery Router Placeholder` before the webhook response. Add Telegram, Email, Slack, persistence, or queue delivery nodes from that point without changing the analysis pipeline."
},
"typeVersion": 1
},
{
"id": "f68d5ff4-9b71-4a4d-8a66-10138440c003",
"name": "Webhook - Investment Research Request",
"type": "n8n-nodes-base.webhook",
"position": [
-848,
80
],
"webhookId": "investment-research-report",
"parameters": {
"path": "investment-research/report",
"options": {
"allowedOrigins": "*"
},
"httpMethod": "POST",
"responseMode": "responseNode"
},
"typeVersion": 2
},
{
"id": "f68d5ff4-9b71-4a4d-8a66-10138440c004",
"name": "Code - Validate Input",
"type": "n8n-nodes-base.code",
"position": [
-608,
80
],
"parameters": {
"jsCode": "const body = $json.body ?? $json;\nconst errors = [];\n\nif (!body || typeof body !== 'object') {\n errors.push({ field: 'body', message: 'Request body must be a JSON object.' });\n}\n\nif (!Array.isArray(body.symbols)) {\n errors.push({ field: 'symbols', message: 'symbols must be an array.' });\n} else if (body.symbols.length === 0) {\n errors.push({ field: 'symbols', message: 'symbols must contain at least one stock symbol.' });\n}\n\nif (!body.reportId || typeof body.reportId !== 'string' || body.reportId.trim().length === 0) {\n errors.push({ field: 'reportId', message: 'reportId is required.' });\n}\n\nconst symbols = Array.isArray(body.symbols)\n ? [...new Set(body.symbols.map((symbol) => String(symbol).trim().toUpperCase()).filter(Boolean))]\n : [];\n\nif (Array.isArray(body.symbols) && symbols.length === 0) {\n errors.push({ field: 'symbols', message: 'symbols must contain at least one non-empty value.' });\n}\n\nif (symbols.length > 25) {\n errors.push({ field: 'symbols', message: 'A maximum of 25 symbols is allowed per request.' });\n}\n\nif (errors.length > 0) {\n return [{\n json: {\n isValid: false,\n success: false,\n statusCode: 400,\n error: {\n code: 'INVALID_REQUEST',\n message: 'Invalid investment research request.',\n details: errors\n }\n }\n }];\n}\n\nreturn [{\n json: {\n isValid: true,\n request: {\n reportId: body.reportId.trim(),\n analysisType: body.analysisType || 'long_term',\n symbols\n },\n receivedAt: new Date().toISOString()\n }\n}];"
},
"typeVersion": 2
},
{
"id": "f68d5ff4-9b71-4a4d-8a66-10138440c005",
"name": "IF - Input Valid?",
"type": "n8n-nodes-base.if",
"position": [
-368,
80
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "valid-input-condition",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.isValid }}",
"rightValue": true
}
]
}
},
"typeVersion": 2.2
},
{
"id": "f68d5ff4-9b71-4a4d-8a66-10138440c006",
"name": "Respond - Invalid Request",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
-128,
224
],
"parameters": {
"options": {
"responseCode": 400
},
"respondWith": "json",
"responseBody": "={{ $json }}"
},
"typeVersion": 1.4
},
{
"id": "f68d5ff4-9b71-4a4d-8a66-10138440c007",
"name": "Code - Prepare Symbol Items",
"type": "n8n-nodes-base.code",
"position": [
-128,
-48
],
"parameters": {
"jsCode": "const request = $json.request;\nreturn request.symbols.map((symbol, index) => ({\n json: {\n request,\n symbol,\n symbolIndex: index,\n startedAt: new Date().toISOString(),\n externalErrors: []\n }\n}));"
},
"typeVersion": 2
},
{
"id": "f68d5ff4-9b71-4a4d-8a66-10138440c008",
"name": "HTTP - Fetch Market Quote (Twelve Data)",
"type": "n8n-nodes-base.httpRequest",
"maxTries": 3,
"position": [
144,
-48
],
"parameters": {
"url": "https://api.twelvedata.com/quote",
"options": {
"timeout": 30000
},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "symbol",
"value": "={{ $json.symbol }}"
},
{
"name": "apikey",
"value": "={{ $env.TWELVE_DATA_API_KEY }}"
}
]
}
},
"retryOnFail": true,
"typeVersion": 4.2,
"continueOnFail": true,
"waitBetweenTries": 2000
},
{
"id": "f68d5ff4-9b71-4a4d-8a66-10138440c009",
"name": "HTTP - Fetch Daily Prices (Twelve Data)",
"type": "n8n-nodes-base.httpRequest",
"maxTries": 3,
"position": [
400,
-48
],
"parameters": {
"url": "https://api.twelvedata.com/time_series",
"options": {
"timeout": 30000
},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "symbol",
"value": "={{ $('Code - Prepare Symbol Items').item.json.symbol }}"
},
{
"name": "interval",
"value": "1day"
},
{
"name": "outputsize",
"value": "220"
},
{
"name": "apikey",
"value": "={{ $env.TWELVE_DATA_API_KEY }}"
}
]
}
},
"retryOnFail": true,
"typeVersion": 4.2,
"continueOnFail": true,
"waitBetweenTries": 2000
},
{
"id": "f68d5ff4-9b71-4a4d-8a66-10138440c010",
"name": "Code - Build Market Data",
"type": "n8n-nodes-base.code",
"position": [
672,
-48
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const base = $('Code - Prepare Symbol Items').item.json;\nconst quote = $('HTTP - Fetch Market Quote (Twelve Data)').item.json ?? {};\nconst timeSeries = $json ?? {};\nconst externalErrors = [...(base.externalErrors || [])];\n\nconst numberOrNull = (value) => {\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : null;\n};\n\nconst avg = (values) => {\n const clean = values.map(numberOrNull).filter((value) => value !== null);\n if (clean.length === 0) return null;\n return Number((clean.reduce((sum, value) => sum + value, 0) / clean.length).toFixed(2));\n};\n\nif (quote.status === 'error' || quote.code || quote.message || quote.error) {\n externalErrors.push({ provider: 'twelvedata', endpoint: 'quote', symbol: base.symbol, message: quote.message || quote.error || 'Quote request failed.' });\n}\n\nif (timeSeries.status === 'error' || timeSeries.code || timeSeries.message || timeSeries.error) {\n externalErrors.push({ provider: 'twelvedata', endpoint: 'time_series', symbol: base.symbol, message: timeSeries.message || timeSeries.error || 'Time series request failed.' });\n}\n\nconst candles = Array.isArray(timeSeries.values)\n ? timeSeries.values.map((row) => ({\n date: row.datetime,\n open: numberOrNull(row.open),\n high: numberOrNull(row.high),\n low: numberOrNull(row.low),\n close: numberOrNull(row.close),\n volume: numberOrNull(row.volume)\n })).filter((row) => row.close !== null)\n : [];\n\nconst closesNewestFirst = candles.map((row) => row.close);\nconst latestCandle = candles[0] || {};\nconst price = numberOrNull(quote.close) ?? numberOrNull(quote.price) ?? latestCandle.close ?? null;\nconst previousClose = numberOrNull(quote.previous_close) ?? (candles[1] ? candles[1].close : null);\nconst changePercent = numberOrNull(quote.percent_change) ?? (price !== null && previousClose ? Number((((price - previousClose) / previousClose) * 100).toFixed(2)) : null);\n\nreturn {\n json: {\n ...base,\n marketData: {\n symbol: base.symbol,\n price,\n previousClose,\n changePercent,\n volume: numberOrNull(quote.volume) ?? latestCandle.volume ?? null,\n marketCap: numberOrNull(quote.market_cap),\n ma50: closesNewestFirst.length >= 50 ? avg(closesNewestFirst.slice(0, 50)) : null,\n ma200: closesNewestFirst.length >= 200 ? avg(closesNewestFirst.slice(0, 200)) : null,\n currency: quote.currency || null,\n exchange: quote.exchange || null,\n fetchedAt: new Date().toISOString()\n },\n candles,\n externalErrors\n }\n};"
},
"typeVersion": 2
},
{
"id": "f68d5ff4-9b71-4a4d-8a66-10138440c011",
"name": "Code - Calculate Technical Indicators",
"type": "n8n-nodes-base.code",
"position": [
928,
-48
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const candles = Array.isArray($json.candles) ? $json.candles : [];\nconst closes = candles.map((row) => row.close).filter((value) => Number.isFinite(value)).reverse();\nconst volumesNewestFirst = candles.map((row) => row.volume).filter((value) => Number.isFinite(value));\n\nconst avg = (values) => values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : null;\n\nfunction calculateRsi(values, period = 14) {\n if (values.length <= period) return null;\n let gains = 0;\n let losses = 0;\n const window = values.slice(-(period + 1));\n for (let index = 1; index < window.length; index += 1) {\n const diff = window[index] - window[index - 1];\n if (diff >= 0) gains += diff;\n else losses += Math.abs(diff);\n }\n const averageGain = gains / period;\n const averageLoss = losses / period;\n if (averageLoss === 0) return 100;\n const rs = averageGain / averageLoss;\n return Math.round(100 - (100 / (1 + rs)));\n}\n\nconst rsi = calculateRsi(closes);\nconst price = $json.marketData.price;\nconst ma50 = $json.marketData.ma50;\nconst ma200 = $json.marketData.ma200;\nconst close20TradingDaysAgo = closes.length > 20 ? closes[closes.length - 21] : null;\nconst momentumPercent = price !== null && close20TradingDaysAgo ? Number((((price - close20TradingDaysAgo) / close20TradingDaysAgo) * 100).toFixed(2)) : null;\n\nlet movingAverageTrend = 'insufficient_data';\nif (ma50 !== null && ma200 !== null) {\n if (ma50 > ma200 && price >= ma50) movingAverageTrend = 'bullish';\n else if (ma50 < ma200 && price < ma50) movingAverageTrend = 'bearish';\n else movingAverageTrend = 'mixed';\n}\n\nconst latestVolume = volumesNewestFirst[0] ?? null;\nconst average20DayVolume = volumesNewestFirst.length >= 20 ? avg(volumesNewestFirst.slice(0, 20)) : null;\nlet volumeTrend = 'insufficient_data';\nif (latestVolume !== null && average20DayVolume) {\n if (latestVolume > average20DayVolume * 1.25) volumeTrend = 'above_average';\n else if (latestVolume < average20DayVolume * 0.75) volumeTrend = 'below_average';\n else volumeTrend = 'normal';\n}\n\nconst momentum = momentumPercent === null ? 'insufficient_data' : momentumPercent > 2 ? 'positive' : momentumPercent < -2 ? 'negative' : 'neutral';\nlet signal = 'neutral';\nif ((rsi !== null && rsi >= 70) || movingAverageTrend === 'bearish') signal = 'cautious';\nif ((rsi !== null && rsi <= 30) && movingAverageTrend !== 'bearish') signal = 'oversold_watch';\nif (movingAverageTrend === 'bullish' && momentum === 'positive' && rsi !== null && rsi < 70) signal = 'constructive';\n\nreturn {\n json: {\n ...$json,\n technicalSummary: {\n rsi,\n signal,\n momentum,\n momentumPercent,\n movingAverageTrend,\n volumeTrend,\n latestVolume,\n average20DayVolume: average20DayVolume === null ? null : Math.round(average20DayVolume),\n calculatedAt: new Date().toISOString()\n }\n }\n};"
},
"typeVersion": 2
},
{
"id": "f68d5ff4-9b71-4a4d-8a66-10138440c012",
"name": "HTTP - Fetch Recent News",
"type": "n8n-nodes-base.httpRequest",
"maxTries": 3,
"position": [
1184,
-48
],
"parameters": {
"url": "https://newsapi.org/v2/everything",
"options": {
"timeout": 30000
},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "q",
"value": "={{ $json.symbol + ' stock OR shares OR earnings' }}"
},
{
"name": "language",
"value": "en"
},
{
"name": "sortBy",
"value": "publishedAt"
},
{
"name": "pageSize",
"value": "10"
},
{
"name": "apiKey",
"value": "={{ $env.NEWS_API_KEY }}"
}
]
}
},
"retryOnFail": true,
"typeVersion": 4.2,
"continueOnFail": true,
"waitBetweenTries": 2000
},
{
"id": "f68d5ff4-9b71-4a4d-8a66-10138440c013",
"name": "Code - Normalize News Articles",
"type": "n8n-nodes-base.code",
"position": [
1440,
-48
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const base = $('Code - Calculate Technical Indicators').item.json;\nconst response = $json ?? {};\nconst externalErrors = [...(base.externalErrors || [])];\n\nconst providerMessage = response.message || response.error?.message || response.error || response.code || null;\nif ((response.status && response.status !== 'ok') || providerMessage) {\n externalErrors.push({\n provider: 'newsapi',\n endpoint: 'everything',\n symbol: base.symbol,\n message: String(providerMessage || 'News request failed.')\n });\n}\n\nconst articles = Array.isArray(response.articles) ? response.articles.slice(0, 10) : [];\nconst newsArticles = articles.map((article) => ({\n title: article.title || '',\n source: article.source?.name || 'Unknown',\n url: article.url || '',\n publishedAt: article.publishedAt || null\n})).filter((article) => article.title || article.url);\n\nreturn {\n json: {\n ...base,\n newsArticles,\n newsCollection: {\n provider: 'newsapi',\n count: newsArticles.length,\n fetchedAt: new Date().toISOString()\n },\n externalErrors\n }\n};"
},
"typeVersion": 2
},
{
"id": "f68d5ff4-9b71-4a4d-8a66-10138440c014",
"name": "AI Agent - News Summarization",
"type": "@n8n/n8n-nodes-langchain.agent",
"maxTries": 2,
"position": [
1712,
-48
],
"parameters": {
"text": "=Symbol: {{ $json.symbol }}\nRecent news articles JSON:\n{{ JSON.stringify($json.newsArticles) }}",
"options": {
"systemMessage": "You are a news summarization agent for an investment research workflow. Use only the provided article titles, sources, URLs, and dates. Do not infer facts that are not present. Output valid JSON only with this exact schema: {\"positiveFactors\":[],\"negativeFactors\":[],\"majorThemes\":[],\"summary\":\"\"}. Keep factors concise and evidence-based."
},
"promptType": "define"
},
"retryOnFail": true,
"typeVersion": 2.2,
"continueOnFail": true,
"waitBetweenTries": 3000
},
{
"id": "f68d5ff4-9b71-4a4d-8a66-10138440c016",
"name": "Code - Parse News Agent Output",
"type": "n8n-nodes-base.code",
"position": [
2032,
-48
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const base = $('Code - Normalize News Articles').item.json;\nconst externalErrors = [...(base.externalErrors || [])];\nconst raw = $json.output ?? $json.text ?? $json.response ?? $json.message ?? '';\n\nfunction parseJson(value) {\n if (value && typeof value === 'object') return value;\n if (typeof value !== 'string') return null;\n try { return JSON.parse(value); } catch (error) {}\n const match = value.match(/\\{[\\s\\S]*\\}/);\n if (!match) return null;\n try { return JSON.parse(match[0]); } catch (error) { return null; }\n}\n\nlet parsed = parseJson(raw);\nif (!parsed) {\n externalErrors.push({ provider: 'openai', endpoint: 'news_agent', symbol: base.symbol, message: 'News summarization output was not valid JSON.' });\n parsed = { positiveFactors: [], negativeFactors: [], majorThemes: [], summary: 'News summary unavailable from the AI agent output.' };\n}\n\nreturn {\n json: {\n ...base,\n newsSummary: {\n positiveFactors: Array.isArray(parsed.positiveFactors) ? parsed.positiveFactors : [],\n negativeFactors: Array.isArray(parsed.negativeFactors) ? parsed.negativeFactors : [],\n majorThemes: Array.isArray(parsed.majorThemes) ? parsed.majorThemes : [],\n summary: typeof parsed.summary === 'string' ? parsed.summary : ''\n },\n externalErrors\n }\n};"
},
"typeVersion": 2
},
{
"id": "f68d5ff4-9b71-4a4d-8a66-10138440c017",
"name": "AI Agent - Investment Analysis",
"type": "@n8n/n8n-nodes-langchain.agent",
"maxTries": 2,
"position": [
2288,
-48
],
"parameters": {
"text": "=Symbol: {{ $json.symbol }}\nAnalysis type: {{ $json.request.analysisType }}\nMarket data JSON:\n{{ JSON.stringify($json.marketData) }}\nTechnical summary JSON:\n{{ JSON.stringify($json.technicalSummary) }}\nNews summary JSON:\n{{ JSON.stringify($json.newsSummary) }}",
"options": {
"systemMessage": "You are an investment research analyst producing internal research, not financial advice. Never tell the reader to buy, sell, or hold. Use only the provided market data, technical indicators, and news summary. Avoid hallucinating facts, cite reasoning from the supplied evidence, and acknowledge missing or weak evidence. Output valid JSON only with this exact schema: {\"score\":0,\"confidence\":0,\"summary\":\"\",\"opportunities\":[],\"risks\":[],\"evidence\":{\"technical\":[],\"news\":[],\"market\":[]}}. score and confidence must be integers from 0 to 100."
},
"promptType": "define"
},
"retryOnFail": true,
"typeVersion": 2.2,
"continueOnFail": true,
"waitBetweenTries": 3000
},
{
"id": "f68d5ff4-9b71-4a4d-8a66-10138440c019",
"name": "Code - Parse Investment Agent Output",
"type": "n8n-nodes-base.code",
"position": [
2608,
-48
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const base = $('Code - Parse News Agent Output').item.json;\nconst externalErrors = [...(base.externalErrors || [])];\nconst raw = $json.output ?? $json.text ?? $json.response ?? $json.message ?? '';\n\nfunction parseJson(value) {\n if (value && typeof value === 'object') return value;\n if (typeof value !== 'string') return null;\n try { return JSON.parse(value); } catch (error) {}\n const match = value.match(/\\{[\\s\\S]*\\}/);\n if (!match) return null;\n try { return JSON.parse(match[0]); } catch (error) { return null; }\n}\n\nfunction clampScore(value) {\n const parsed = Math.round(Number(value));\n if (!Number.isFinite(parsed)) return 0;\n return Math.max(0, Math.min(100, parsed));\n}\n\nlet parsed = parseJson(raw);\nif (!parsed) {\n externalErrors.push({ provider: 'openai', endpoint: 'investment_agent', symbol: base.symbol, message: 'Investment analysis output was not valid JSON.' });\n parsed = {\n score: 0,\n confidence: 0,\n summary: 'Investment analysis unavailable from the AI agent output.',\n opportunities: [],\n risks: [],\n evidence: { technical: [], news: [], market: [] }\n };\n}\n\nreturn {\n json: {\n symbol: base.symbol,\n request: base.request,\n marketData: base.marketData,\n technicalSummary: base.technicalSummary,\n newsArticles: base.newsArticles,\n newsSummary: base.newsSummary,\n analysis: {\n score: clampScore(parsed.score),\n confidence: clampScore(parsed.confidence),\n summary: typeof parsed.summary === 'string' ? parsed.summary : '',\n opportunities: Array.isArray(parsed.opportunities) ? parsed.opportunities : [],\n risks: Array.isArray(parsed.risks) ? parsed.risks : [],\n evidence: {\n technical: Array.isArray(parsed.evidence?.technical) ? parsed.evidence.technical : [],\n news: Array.isArray(parsed.evidence?.news) ? parsed.evidence.news : [],\n market: Array.isArray(parsed.evidence?.market) ? parsed.evidence.market : []\n }\n },\n externalErrors,\n completedAt: new Date().toISOString()\n }\n};"
},
"typeVersion": 2
},
{
"id": "f68d5ff4-9b71-4a4d-8a66-10138440c020",
"name": "Code - Aggregate Final Report",
"type": "n8n-nodes-base.code",
"position": [
2880,
-48
],
"parameters": {
"jsCode": "const items = $input.all().map((item) => item.json);\nconst request = items[0]?.request || { reportId: null, analysisType: null, symbols: [] };\nconst sortedSymbols = [...items].sort((a, b) => (b.analysis?.score ?? 0) - (a.analysis?.score ?? 0));\nconst requestedSymbolCount = Array.isArray(request.symbols) ? request.symbols.length : sortedSymbols.length;\nconst failedSymbols = sortedSymbols.filter((item) => Array.isArray(item.externalErrors) && item.externalErrors.length > 0).length;\nconst status = failedSymbols === 0 ? 'complete' : failedSymbols === sortedSymbols.length ? 'failed' : 'partial';\n\nconst report = {\n reportId: request.reportId,\n generatedAt: new Date().toISOString(),\n analysisType: request.analysisType,\n status,\n symbols: sortedSymbols.map((item) => ({\n symbol: item.symbol,\n analysis: item.analysis,\n marketData: item.marketData,\n technicalSummary: item.technicalSummary,\n newsSummary: item.newsSummary,\n newsArticles: item.newsArticles,\n errors: item.externalErrors || []\n })),\n metadata: {\n requestedSymbolCount,\n processedSymbolCount: sortedSymbols.length,\n failedSymbols,\n sortedBy: 'analysis.score:desc',\n disclaimer: 'For research purposes only. This report is not financial advice.'\n }\n};\n\nreturn [{\n json: {\n success: status !== 'failed',\n status,\n reportId: request.reportId,\n report\n }\n}];"
},
"typeVersion": 2
},
{
"id": "f68d5ff4-9b71-4a4d-8a66-10138440c021",
"name": "IF - Report Has Symbol Errors?",
"type": "n8n-nodes-base.if",
"position": [
3152,
-48
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "symbol-error-condition",
"operator": {
"type": "number",
"operation": "gt"
},
"leftValue": "={{ $json.report.metadata.failedSymbols }}",
"rightValue": 0
}
]
}
},
"typeVersion": 2.2
},
{
"id": "f68d5ff4-9b71-4a4d-8a66-10138440c022",
"name": "Code - Delivery Router Placeholder",
"type": "n8n-nodes-base.code",
"position": [
3440,
-48
],
"parameters": {
"jsCode": "return $input.all().map((item) => ({\n json: {\n ...item.json,\n delivery: {\n webhookResponse: true,\n telegramQueued: false,\n emailQueued: false,\n extensionNode: 'Add Telegram or Email delivery nodes after this node.'\n }\n }\n}));"
},
"typeVersion": 2
},
{
"id": "f68d5ff4-9b71-4a4d-8a66-10138440c023",
"name": "Respond - Research Report",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
3728,
-48
],
"parameters": {
"options": {
"responseCode": 200
},
"respondWith": "json",
"responseBody": "={{ { success: $json.success, status: $json.status, reportId: $json.reportId, report: $json.report } }}"
},
"typeVersion": 1.4
},
{
"id": "b8517080-2f36-4bf3-8c2d-1f3643aace90",
"name": "Google Gemini Chat Model - News",
"type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
"position": [
1712,
160
],
"parameters": {
"options": {
"temperature": 0.1,
"maxOutputTokens": 900
},
"modelName": "models/gemini-3.1-flash-lite"
},
"credentials": {
"googlePalmApi": {
"id": "f4UlQ2r9PCxHhGRT",
"name": "Google Gemini(PaLM) Api account 2"
}
},
"typeVersion": 1
},
{
"id": "377717ad-1d17-4fda-8cbb-ec37307c65d6",
"name": "Google Gemini Chat Model - Investment",
"type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
"position": [
2288,
160
],
"parameters": {
"options": {
"temperature": 0.2,
"maxOutputTokens": 1400
},
"modelName": "models/gemini-3.1-flash-lite"
},
"credentials": {
"googlePalmApi": {
"id": "f4UlQ2r9PCxHhGRT",
"name": "Google Gemini(PaLM) Api account 2"
}
},
"typeVersion": 1
}
],
"active": true,
"pinData": {},
"settings": {
"timezone": "Asia/Seoul",
"callerPolicy": "workflowsFromSameOwner",
"timeSavedMode": "fixed",
"availableInMCP": true,
"executionOrder": "v1",
"executionTimeout": 60,
"saveManualExecutions": true,
"timeSavedPerExecution": 10
},
"versionId": "50a32d10-2dd4-41d2-970a-417894221459",
"connections": {
"IF - Input Valid?": {
"main": [
[
{
"node": "Code - Prepare Symbol Items",
"type": "main",
"index": 0
}
],
[
{
"node": "Respond - Invalid Request",
"type": "main",
"index": 0
}
]
]
},
"Code - Validate Input": {
"main": [
[
{
"node": "IF - Input Valid?",
"type": "main",
"index": 0
}
]
]
},
"Code - Build Market Data": {
"main": [
[
{
"node": "Code - Calculate Technical Indicators",
"type": "main",
"index": 0
}
]
]
},
"HTTP - Fetch Recent News": {
"main": [
[
{
"node": "Code - Normalize News Articles",
"type": "main",
"index": 0
}
]
]
},
"Code - Prepare Symbol Items": {
"main": [
[
{
"node": "HTTP - Fetch Market Quote (Twelve Data)",
"type": "main",
"index": 0
}
]
]
},
"AI Agent - News Summarization": {
"main": [
[
{
"node": "Code - Parse News Agent Output",
"type": "main",
"index": 0
}
]
]
},
"Code - Aggregate Final Report": {
"main": [
[
{
"node": "IF - Report Has Symbol Errors?",
"type": "main",
"index": 0
}
]
]
},
"AI Agent - Investment Analysis": {
"main": [
[
{
"node": "Code - Parse Investment Agent Output",
"type": "main",
"index": 0
}
]
]
},
"Code - Normalize News Articles": {
"main": [
[
{
"node": "AI Agent - News Summarization",
"type": "main",
"index": 0
}
]
]
},
"Code - Parse News Agent Output": {
"main": [
[
{
"node": "AI Agent - Investment Analysis",
"type": "main",
"index": 0
}
]
]
},
"IF - Report Has Symbol Errors?": {
"main": [
[
{
"node": "Code - Delivery Router Placeholder",
"type": "main",
"index": 0
}
],
[
{
"node": "Code - Delivery Router Placeholder",
"type": "main",
"index": 0
}
]
]
},
"Google Gemini Chat Model - News": {
"ai_languageModel": [
[
{
"node": "AI Agent - News Summarization",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Code - Delivery Router Placeholder": {
"main": [
[
{
"node": "Respond - Research Report",
"type": "main",
"index": 0
}
]
]
},
"Code - Parse Investment Agent Output": {
"main": [
[
{
"node": "Code - Aggregate Final Report",
"type": "main",
"index": 0
}
]
]
},
"Code - Calculate Technical Indicators": {
"main": [
[
{
"node": "HTTP - Fetch Recent News",
"type": "main",
"index": 0
}
]
]
},
"Google Gemini Chat Model - Investment": {
"ai_languageModel": [
[
{
"node": "AI Agent - Investment Analysis",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Webhook - Investment Research Request": {
"main": [
[
{
"node": "Code - Validate Input",
"type": "main",
"index": 0
}
]
]
},
"HTTP - Fetch Daily Prices (Twelve Data)": {
"main": [
[
{
"node": "Code - Build Market Data",
"type": "main",
"index": 0
}
]
]
},
"HTTP - Fetch Market Quote (Twelve Data)": {
"main": [
[
{
"node": "HTTP - Fetch Daily Prices (Twelve Data)",
"type": "main",
"index": 0
}
]
]
}
}
}