This workflow corresponds to n8n.io template #14297 — we link there as the canonical source.
This workflow follows the Gmail → 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": "GbNvmnke6PJsrUTB",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Daily Financial News & Movers Email Digest",
"tags": [],
"nodes": [
{
"id": "node-0001",
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
48,
160
],
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "0 7 * * *"
}
]
}
},
"typeVersion": 1.2
},
{
"id": "node-0002",
"name": "Google Sheets",
"type": "n8n-nodes-base.googleSheets",
"position": [
496,
256
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1k6mDOKB0B6Wt2OOvaAo7oTxpn7Zt-XeiLA0NZlM6VFY/edit#gid=0",
"cachedResultName": "input"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1k6mDOKB0B6Wt2OOvaAo7oTxpn7Zt-XeiLA0NZlM6VFY",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1k6mDOKB0B6Wt2OOvaAo7oTxpn7Zt-XeiLA0NZlM6VFY/edit?usp=drivesdk",
"cachedResultName": "stocks"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.4
},
{
"id": "node-0003",
"name": "HTTP Request - EOD",
"type": "n8n-nodes-base.httpRequest",
"position": [
720,
256
],
"parameters": {
"url": "={{ 'https://eodhd.com/api/eod/' + $json.ticker + '.US' }}",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "api_token",
"value": "={{ $('\u2699\ufe0f Config').first().json.api_token }}"
},
{
"name": "fmt",
"value": "json"
},
{
"name": "order",
"value": "d"
},
{
"name": "from",
"value": "={{ new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString().split('T')[0] }}"
},
{
"name": "to",
"value": "={{ new Date().toISOString().split('T')[0] }}"
}
]
}
},
"typeVersion": 4.2,
"continueOnFail": true
},
{
"id": "node-0004",
"name": "HTTP Request - News",
"type": "n8n-nodes-base.httpRequest",
"position": [
944,
256
],
"parameters": {
"url": "https://eodhd.com/api/news",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "s",
"value": "={{ $('Google Sheets').item.json.ticker + '.US' }}"
},
{
"name": "api_token",
"value": "={{ $('\u2699\ufe0f Config').first().json.api_token }}"
},
{
"name": "fmt",
"value": "json"
},
{
"name": "limit",
"value": "3"
},
{
"name": "from",
"value": "={{ new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0] }}"
}
]
}
},
"typeVersion": 4.2,
"continueOnFail": true
},
{
"id": "node-0005",
"name": "Build Email HTML",
"type": "n8n-nodes-base.code",
"position": [
1168,
256
],
"parameters": {
"jsCode": "const gsItems = $('Google Sheets').all();\nconst eodAll = $('HTTP Request - EOD').all();\nconst newsAll = $('HTTP Request - News').all();\n\n// Helper: handles pairedItem as object, array, or number\nfunction getPairedIdx(item) {\n const pi = item.pairedItem;\n if (pi === null || pi === undefined) return 0;\n if (Array.isArray(pi)) return pi[0]?.item ?? 0;\n if (typeof pi === 'object') return pi.item ?? 0;\n return Number(pi) || 0;\n}\n\n// Group split items back by ticker using pairedItem index\nconst eodByIdx = {};\nconst newsByIdx = {};\n\neodAll.forEach(item => {\n const idx = getPairedIdx(item);\n if (!eodByIdx[idx]) eodByIdx[idx] = [];\n eodByIdx[idx].push(item.json);\n});\n\nnewsAll.forEach(item => {\n const idx = getPairedIdx(item);\n if (!newsByIdx[idx]) newsByIdx[idx] = [];\n newsByIdx[idx].push(item.json);\n});\n\nconst oneWeekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);\nconst results = [];\nconst failedTickers = [];\n\ngsItems.forEach((gsItem, idx) => {\n const ticker = (gsItem.json.ticker || '').toString().trim().toUpperCase();\n if (!ticker) return;\n\n const eodData = eodByIdx[idx] || [];\n const newsData = newsByIdx[idx] || [];\n\n const eodFailed = eodData.length === 0 || eodData[0]?.error !== undefined;\n const newsFailed = newsData.length === 0 || newsData[0]?.error !== undefined;\n\n if (eodFailed && newsFailed) { failedTickers.push(ticker); return; }\n\n // Price & change\n const sorted = [...eodData].sort((a, b) => new Date(b.date) - new Date(a.date));\n let price = null, changePct = null;\n if (sorted.length >= 2) {\n price = sorted[0].close;\n const prev = sorted[1].close;\n changePct = prev !== 0 ? ((price - prev) / prev) * 100 : null;\n } else if (sorted.length === 1) {\n price = sorted[0].close;\n }\n\n // News \u2014 filter to last 7 days\n const news = newsData\n .filter(a => a.date && new Date(a.date) >= oneWeekAgo)\n .slice(0, 5)\n .map(a => {\n const polarity = a.sentiment?.polarity ?? null;\n const sentimentEmoji = polarity === null ? '' : polarity > 0.6 ? '\ud83d\ude0a' : polarity < 0.4 ? '\ud83d\ude1f' : '\ud83d\ude10';\n const sentimentLabel = polarity === null ? '' : polarity > 0.6 ? 'Positivo' : polarity < 0.4 ? 'Negativo' : 'Neutral';\n const sentimentColor = polarity === null ? '#718096' : polarity > 0.6 ? '#276749' : polarity < 0.4 ? '#c53030' : '#b7791f';\n return {\n title: a.title || 'Untitled',\n date: a.date || '',\n url: a.link || a.url || '#',\n summary: (a.content || a.description || '').toString().substring(0, 200),\n tags: (a.tags || []).slice(0, 3),\n sentimentEmoji, sentimentLabel, sentimentColor\n };\n });\n\n if (eodFailed) failedTickers.push(ticker);\n results.push({ ticker, price, changePct, news, error: eodFailed });\n});\n\n// Classify movers\nconst withChange = results.filter(r => r.changePct !== null);\nwithChange.sort((a, b) => Math.abs(b.changePct) - Math.abs(a.changePct));\n\n// \u2500\u2500 Build HTML \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst today = new Date().toLocaleDateString('en-US', { weekday:'long', year:'numeric', month:'long', day:'numeric' });\nconst fmtPrice = v => v != null ? `$${Number(v).toFixed(2)}` : 'N/A';\nconst fmtPct = v => v != null ? `${v >= 0 ? '+' : ''}${Number(v).toFixed(2)}%` : 'N/A';\nconst pctColor = v => v == null ? '#718096' : v >= 0 ? '#276749' : '#c53030';\nconst signal = v => v == null ? '\u26aa' : v >= 2 ? '\ud83d\udfe2' : v <= -2 ? '\ud83d\udd34' : '\ud83d\udfe1';\n\n// Movers table rows\nconst moverRows = results.map(r => `\n <tr style='border-top:1px solid #edf2f7;background:transparent;'>\n <td style='padding:11px 12px;font-weight:700;color:#1a202c;'>${r.ticker}</td>\n <td style='padding:11px 12px;text-align:right;color:#2d3748;'>${fmtPrice(r.price)}</td>\n <td style='padding:11px 12px;text-align:right;color:${pctColor(r.changePct)};font-weight:700;'>${fmtPct(r.changePct)}</td>\n <td style='padding:11px 12px;text-align:center;font-size:18px;'>${signal(r.changePct)}</td>\n </tr>`).join('');\n\n// News sections\nconst newsSections = results.map(r => {\n if (!r.news || r.news.length === 0) return '';\n const articles = r.news.map(a => {\n const tagsHtml = a.tags.length\n ? `<div style='margin-top:5px;'>${a.tags.map(t => `<span style='display:inline-block;background:#edf2f7;color:#4a5568;font-size:10px;padding:2px 7px;border-radius:10px;margin-right:4px;'>${t}</span>`).join('')}</div>`\n : '';\n const sentHtml = a.sentimentLabel\n ? `<span style='font-size:11px;color:${a.sentimentColor};font-weight:600;'>${a.sentimentEmoji} ${a.sentimentLabel}</span>`\n : '';\n return `\n <div style='padding:12px 0;border-bottom:1px solid #f0f2f5;'>\n <div style='display:flex;justify-content:space-between;align-items:flex-start;gap:8px;'>\n <a href='${a.url}' style='font-size:13px;font-weight:600;color:#2b6cb0;text-decoration:none;line-height:1.4;'>${a.title}</a>\n ${sentHtml}\n </div>\n ${a.summary ? `<p style='margin:5px 0 0;font-size:12px;color:#718096;line-height:1.5;'>${a.summary}\u2026</p>` : ''}\n ${tagsHtml}\n <p style='margin:4px 0 0;font-size:11px;color:#a0aec0;'>${a.date ? new Date(a.date).toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'}) : ''}</p>\n </div>`;\n }).join('');\n\n return `\n <div style='background:#fff;border-radius:10px;padding:22px 26px;margin-bottom:16px;box-shadow:0 1px 5px rgba(0,0,0,.07);'>\n <h3 style='margin:0 0 12px;font-size:15px;font-weight:700;color:#2d3748;'>${r.ticker}</h3>\n ${articles}\n </div>`;\n}).join('');\n\nconst failedSection = failedTickers.length\n ? `<p style='color:#a0aec0;font-size:12px;text-align:center;'>\u26a0\ufe0f No data for: ${failedTickers.join(', ')}</p>`\n : '';\n\nconst html = `<!DOCTYPE html><html lang='en'><head><meta charset='UTF-8'></head>\n<body style='font-family:Arial,sans-serif;max-width:700px;margin:0 auto;padding:20px;background:#f0f2f5;'>\n<div style='background:linear-gradient(135deg,#1a1a2e,#16213e);color:white;padding:24px 28px;border-radius:10px;margin-bottom:24px;'>\n <h1 style='margin:0;font-size:22px;font-weight:700;'>\ud83d\udcca Daily Market Digest</h1>\n <p style='margin:8px 0 0;color:#90aec6;font-size:13px;'>${today}</p>\n</div>\n<div style='background:#fff;border-radius:10px;padding:22px 26px;margin-bottom:24px;box-shadow:0 1px 5px rgba(0,0,0,.07);'>\n <h2 style='margin:0 0 18px;font-size:16px;font-weight:700;color:#2d3748;border-bottom:2px solid #e8ecf0;padding-bottom:12px;'>\ud83d\udcc8 Top Movers</h2>\n <table style='width:100%;border-collapse:collapse;font-size:14px;'>\n <thead><tr style='background:#f7f9fc;'>\n <th style='padding:9px 12px;text-align:left;color:#718096;font-size:11px;text-transform:uppercase;'>Ticker</th>\n <th style='padding:9px 12px;text-align:right;color:#718096;font-size:11px;text-transform:uppercase;'>Last Close</th>\n <th style='padding:9px 12px;text-align:right;color:#718096;font-size:11px;text-transform:uppercase;'>Change %</th>\n <th style='padding:9px 12px;text-align:center;color:#718096;font-size:11px;text-transform:uppercase;'>Signal</th>\n </tr></thead>\n <tbody>${moverRows}</tbody>\n </table>\n</div>\n<div style='background:#fff;border-radius:10px;padding:22px 26px;margin-bottom:24px;box-shadow:0 1px 5px rgba(0,0,0,.07);'>\n <h2 style='margin:0 0 18px;font-size:16px;font-weight:700;color:#2d3748;border-bottom:2px solid #e8ecf0;padding-bottom:12px;'>\ud83d\udcf0 News \u2014 Last 7 Days</h2>\n ${newsSections || '<p style=\"color:#a0aec0;font-size:13px;\">No news found for the selected period.</p>'}\n</div>\n${failedSection}\n<p style='text-align:center;color:#a0aec0;font-size:11px;margin-top:8px;'>Generated by n8n \u00b7 Data by EODHD</p>\n</body></html>`;\n\nreturn [{\n json: {\n html,\n subject: `\ud83d\udcca Daily Market Digest \u2014 ${new Date().toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'})}`\n }\n}];\n"
},
"typeVersion": 2
},
{
"id": "node-0006",
"name": "Send Email via Gmail",
"type": "n8n-nodes-base.gmail",
"position": [
1392,
256
],
"parameters": {
"sendTo": "={{ $('\u2699\ufe0f Config').first().json.recipient_email }}",
"message": "={{ $json.html }}",
"options": {},
"subject": "={{ $json.subject }}"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.1
},
{
"id": "b369108f-37f6-4d5d-9f70-3d06f3f2e620",
"name": "When clicking \u2018Execute workflow\u2019",
"type": "n8n-nodes-base.manualTrigger",
"position": [
272,
352
],
"parameters": {},
"typeVersion": 1
},
{
"id": "node-cfg-0010",
"name": "\u2699\ufe0f Config",
"type": "n8n-nodes-base.set",
"position": [
272,
160
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "cfg-a1",
"name": "api_token",
"type": "string",
"value": "YOUR_EODHD_API_KEY"
},
{
"id": "cfg-a2",
"name": "recipient_email",
"type": "string",
"value": "your@email.com"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "sticky-0001",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1008,
-48
],
"parameters": {
"width": 684,
"height": 514,
"content": "## Send daily stock price movers and financial news digest via Gmail\n\nAutomatically monitor your stock watchlist every morning and receive a formatted email with price changes and curated financial news with sentiment analysis.\n\n**How it works:**\n1. Schedule Trigger fires at 7 AM daily\n2. Reads tickers from Google Sheets (one ticker per row, column `ticker`)\n3. Fetches EOD prices + last 7 days of news from EODHD API per ticker\n4. Calculates daily % change and classifies news sentiment (Positive / Neutral / Negative)\n5. Sends an HTML digest via Gmail with a movers table and news per ticker\n\n**Setup \u2014 4 steps:**\n1. Open **\u2699\ufe0f Config** and fill in your `api_token` (EODHD) and `recipient_email`\n2. Connect **Google Sheets** credential \u2192 set your spreadsheet and sheet name (column must be named `ticker`)\n3. Connect **Gmail** credential\n4. Set your timezone in **Settings \u2192 Workflow Settings**\n\n**Requirements:** EODHD account (free tier works) \u00b7 Google OAuth2 configured in n8n"
},
"typeVersion": 1
},
{
"id": "sticky-0002",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-240,
64
],
"parameters": {
"color": 7,
"width": 214,
"height": 330,
"content": "## Step 1 \u2014 Trigger & config\n\nSet your **EODHD API key** and **recipient email** in \u2699\ufe0f Config before running.\n\nRuns daily at **7 AM UTC**. Change timezone in Workflow Settings.\nYou can also run manually with the \"Execute workflow\" button."
},
"typeVersion": 1
},
{
"id": "sticky-0003",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
608,
-176
],
"parameters": {
"color": 7,
"height": 362,
"content": "## Step 2 \u2014 Fetch prices & news (EODHD API)\n\nYour Google Sheet must have a column named **`ticker`** (lowercase). One US ticker per row (MSFT, META, AMZN\u2026).\n\n- **EOD**: fetches last 14 days of closing prices \u2014 calculates daily % change\n- **News**: fetches last 7 days of articles with sentiment polarity score per ticker"
},
"typeVersion": 1
},
{
"id": "sticky-0004",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
1040,
-160
],
"parameters": {
"color": 7,
"width": 336,
"height": 298,
"content": "## Step 3 \u2014 Build & send email\n\nGroups prices and news by ticker, calculates % change vs previous close, and classifies sentiment:\n- \ud83d\ude0a **Positive** (polarity > 0.6)\n- \ud83d\ude10 **Neutral**\n- \ud83d\ude1f **Negative** (polarity < 0.4)\n\nSends the HTML digest to the email set in \u2699\ufe0f Config."
},
"typeVersion": 1
},
{
"id": "f7898c1c-4b94-4d85-ad28-2172c03be622",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
1584,
48
],
"parameters": {
"height": 288,
"content": "## Output\n"
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"callerPolicy": "workflowsFromSameOwner",
"availableInMCP": false,
"executionOrder": "v1"
},
"versionId": "c46ee938-f6c0-491f-97e2-6ebdfcc2d912",
"connections": {
"Google Sheets": {
"main": [
[
{
"node": "HTTP Request - EOD",
"type": "main",
"index": 0
}
]
]
},
"\u2699\ufe0f Config": {
"main": [
[
{
"node": "Google Sheets",
"type": "main",
"index": 0
}
]
]
},
"Build Email HTML": {
"main": [
[
{
"node": "Send Email via Gmail",
"type": "main",
"index": 0
}
]
]
},
"Schedule Trigger": {
"main": [
[
{
"node": "\u2699\ufe0f Config",
"type": "main",
"index": 0
}
]
]
},
"HTTP Request - EOD": {
"main": [
[
{
"node": "HTTP Request - News",
"type": "main",
"index": 0
}
]
]
},
"HTTP Request - News": {
"main": [
[
{
"node": "Build Email HTML",
"type": "main",
"index": 0
}
]
]
},
"When clicking 'Execute workflow'": {
"main": [
[
{
"node": "\u2699\ufe0f Config",
"type": "main",
"index": 0
}
]
]
},
"When clicking \u2018Execute workflow\u2019": {
"main": [
[
{
"node": "Google Sheets",
"type": "main",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
gmailOAuth2googleSheetsOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow automatically generates a daily stock market email digest, combining price movements and recent financial news into a clean, actionable report.
Source: https://n8n.io/workflows/14297/ — 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.
YOUR_ID 4. Uses gmail, googleDrive, googleSheets, httpRequest. Scheduled trigger; 53 nodes.
Looking for a way to track GitHub bounty issues automatically and get notified in real time? This GitHub Bounty Tracker workflow monitors repositories for issues labeled 💎 Bounty, logs them in Google
This workflow automatically sends a beautifully designed HTML newsletter every Sunday at 8 AM, featuring products currently on sale from your Algolia-powered e-commerce store.
This n8n template demonstrates how to build a Auto Lead Gen & Outreach System for Local Businesses specifically designed to help businesses that don’t have a website yet.
I created this workflow with care for marketing professionals and agencies who manage multiple Meta Ads (Facebook) accounts and want to track ad account balances automatically — no more logging in eve