This workflow corresponds to n8n.io template #7252 — we link there as the canonical source.
This workflow follows the Google Sheets → HTTP Request 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 →
{
"nodes": [
{
"id": "9a8548ad-12f1-41a7-9226-c8d06806b7b2",
"name": "3. Extract All Player IDs",
"type": "n8n-nodes-base.code",
"notes": "who will be playing today.",
"position": [
32,
-128
],
"parameters": {
"jsCode": "const allPlayerIds = new Set();\nconst allGames = items[0].json.dates[0]?.games || [];\n\nif (allGames.length === 0) {\n return [];\n}\n\n// Loop through all games to find every player\nfor (const game of allGames) {\n if (game.teams?.home?.probablePitcher?.id) {\n allPlayerIds.add(game.teams.home.probablePitcher.id);\n }\n if (game.teams?.away?.probablePitcher?.id) {\n allPlayerIds.add(game.teams.away.probablePitcher.id);\n }\n if (game.lineups && Array.isArray(game.lineups.homePlayers)) {\n for (const player of game.lineups.homePlayers) {\n if (player.id) allPlayerIds.add(player.id);\n }\n }\n if (game.lineups && Array.isArray(game.lineups.awayPlayers)) {\n for (const player of game.lineups.awayPlayers) {\n if (player.id) allPlayerIds.add(player.id);\n }\n }\n}\n\nif (allPlayerIds.size === 0) {\n return [];\n}\n\n// We pass the original game data THROUGH this node by adding it to the output\nconst output = items[0].json;\noutput.playerIdsString = Array.from(allPlayerIds).join(',');\nreturn [{ json: output }];"
},
"notesInFlow": true,
"typeVersion": 2
},
{
"id": "a0e5e8f0-e2fe-415e-92f4-cb4e29a6fd72",
"name": "4. Get Batched Player Stats",
"type": "n8n-nodes-base.httpRequest",
"position": [
256,
-128
],
"parameters": {
"url": "https://statsapi.mlb.com/api/v1/people",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "personIds",
"value": "={{$json.playerIdsString}}"
},
{
"name": "hydrate",
"value": "stats(group=[pitching,hitting,fielding],type=[season])"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "ff23a51f-3dc8-4b85-97b0-611e0bb6a304",
"name": "2. Get Daily Games",
"type": "n8n-nodes-base.httpRequest",
"position": [
-176,
-128
],
"parameters": {
"url": "https://statsapi.mlb.com/api/v1/schedule",
"options": {
"response": {
"response": {
"responseFormat": "json"
}
}
},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "sportId",
"value": "1"
},
{
"name": "date",
"value": "={{ $now.toFormat('yyyy-MM-dd') }}"
},
{
"name": "hydrate",
"value": "probablePitcher,lineups"
}
]
}
},
"typeVersion": 4.1
},
{
"id": "32ccdb2c-0b9d-4af4-9590-c58b9902c31c",
"name": "5. Create Final Matchup Rows",
"type": "n8n-nodes-base.code",
"notes": "game schedule data and merges it with the detailed player stats",
"position": [
496,
-128
],
"parameters": {
"jsCode": "const allMatchupRows = [];\n\n// Get player stats from the direct input of this node\nconst playerStatsData = items[0].json;\n\n// Get the original game data by looking back at the previous node\nconst scheduleNode = $('3. Extract All Player IDs').first();\nconst originalScheduleData = scheduleNode.json;\nconst games = originalScheduleData.dates[0]?.games || [];\n\nconst playersWithStats = playerStatsData.people || [];\n\nif (games.length === 0 || playersWithStats.length === 0) {\n return [];\n}\n\nconst statsMap = new Map(playersWithStats.map(p => [p.id, p]));\n\nfor (const game of games) {\n\n const gameStartTime = game.gameDate || 'N/A';\n\n // --- Home Pitcher vs Away Batters ---\n const homePitcherId = game.teams?.home?.probablePitcher?.id;\n const awayLineup = game.lineups?.awayPlayers;\n\n if (homePitcherId && Array.isArray(awayLineup)) {\n const pitcherData = statsMap.get(homePitcherId);\n if (pitcherData) {\n for (const batter of awayLineup) {\n const batterData = statsMap.get(batter.id);\n if (batterData) {\n const pitcherStats = pitcherData.stats?.find(s => s.group?.displayName === 'pitching')?.splits[0]?.stat || {};\n const batterStats = batterData.stats?.find(s => s.group?.displayName === 'hitting')?.splits[0]?.stat || {};\n allMatchupRows.push({\n gameStartTime: gameStartTime,\n gameDate: game.officialDate || 'N/A',\n pitcherName: pitcherData.fullName || 'N/A',\n pitcherTeam: game.teams?.home?.team?.name || 'N/A',\n pitcherThrows: pitcherData.pitchHand?.description || 'N/A',\n pitcherERA: pitcherStats.era,\n pitcherSO: pitcherStats.strikeOuts,\n opponent: game.teams?.away?.team?.name || 'N/A',\n batterName: batterData.fullName || 'N/A',\n batterPosition: batter.primaryPosition?.abbreviation || 'N/A',\n batterBats: batterData.batSide?.description || 'N/A',\n batterAVG: batterStats.avg,\n batterOPS: batterStats.ops,\n batterHR: batterStats.homeRuns,\n batterHits: batterStats.hits,\n batterRBI: batterStats.rbi,\n pitcherId: pitcherData.id, // ADDED: Pitcher ID\n batterId: batterData.id, // ADDED: Batter ID\n });\n }\n }\n }\n }\n\n // --- Away Pitcher vs Home Batters ---\n const awayPitcherId = game.teams?.away?.probablePitcher?.id;\n const homeLineup = game.lineups?.homePlayers;\n\n if (awayPitcherId && Array.isArray(homeLineup)) {\n const pitcherData = statsMap.get(awayPitcherId);\n if (pitcherData) {\n for (const batter of homeLineup) {\n const batterData = statsMap.get(batter.id);\n if (batterData) {\n const pitcherStats = pitcherData.stats?.find(s => s.group?.displayName === 'pitching')?.splits[0]?.stat || {};\n const batterStats = batterData.stats?.find(s => s.group?.displayName === 'hitting')?.splits[0]?.stat || {};\n allMatchupRows.push({\n gameStartTime: gameStartTime,\n gameDate: game.officialDate || 'N/A',\n pitcherName: pitcherData.fullName || 'N/A',\n pitcherTeam: game.teams?.away?.team?.name || 'N/A',\n pitcherThrows: pitcherData.pitchHand?.description || 'N/A',\n pitcherERA: pitcherStats.era,\n pitcherSO: pitcherStats.strikeOuts,\n opponent: game.teams?.home?.team?.name || 'N/A',\n batterName: batterData.fullName || 'N/A',\n batterPosition: batter.primaryPosition?.abbreviation || 'N/A',\n batterBats: batterData.batSide?.description || 'N/A',\n batterAVG: batterStats.avg,\n batterOPS: batterStats.ops,\n batterHR: batterStats.homeRuns,\n batterHits: batterStats.hits,\n batterRBI: batterStats.rbi,\n pitcherId: pitcherData.id, // ADDED: Pitcher ID\n batterId: batterData.id, // ADDED: Batter ID\n });\n }\n }\n }\n }\n}\n\nreturn allMatchupRows.map(row => ({ json: row }));"
},
"notesInFlow": true,
"typeVersion": 2,
"alwaysOutputData": true
},
{
"id": "73bac962-fa64-45dc-86bd-671a17488992",
"name": "6. Filter for Top Matchups",
"type": "n8n-nodes-base.code",
"notes": "Pandas to filter and sort before Sheets",
"position": [
736,
-128
],
"parameters": {
"jsCode": "// Convert to Eastern Time using proper timezone handling\nfunction convertToEasternTime(isoString) {\n\tconst date = new Date(isoString);\n\treturn date.toLocaleTimeString('en-US', {\n\t\thour: '2-digit',\n\t\tminute: '2-digit',\n\t\thour12: true,\n\t\ttimeZone: 'America/New_York'\n\t});\n}\n\nconst HEADER_ORDER = [\n\t\"gameDate\",\n\t\"gameStartTime\",\n\t\"pitcherId\",\n\t\"pitcherName\",\n\t\"pitcherTeam\",\n\t\"pitcherSO\",\n\t\"pitcherERA\",\n\t\"pitcherThrows\",\n\t\"batterBats\",\n\t\"opponent\",\n\t\"batterName\",\n\t\"batterAVG\",\n\t\"batterHR\",\n\t\"batterHits\",\n\t\"batterOPS\",\n\t\"batterRBI\",\n\t\"batterId\",\n\t\"batterPosition\"\n];\n\nconst allMatchups = items.map(item => item.json);\n\n// Filter valid rows\nconst valid = allMatchups.filter(m => {\n\tconst era = parseFloat(m.pitcherERA);\n\tconst ops = parseFloat(m.batterOPS);\n\treturn !isNaN(era) && era > 3.33 && !isNaN(ops) && m.gameStartTime;\n});\n\n// Parse and add helper fields\nvalid.forEach(m => {\n\tm.era = parseFloat(m.pitcherERA);\n\tm.ops = parseFloat(m.batterOPS);\n\tm.gameStartObj = new Date(m.gameStartTime);\n});\n\n// Step 1: Get top 9 unique pitchers with highest ERA\nconst seenPitchers = new Set();\nconst topPitchers = [];\n\nvalid\n\t.sort((a, b) => b.era - a.era)\n\t.forEach(m => {\n\t\tif (!seenPitchers.has(m.pitcherName)) {\n\t\t\tseenPitchers.add(m.pitcherName);\n\t\t\ttopPitchers.push(m.pitcherName);\n\t\t}\n\t});\n\nconst top9 = topPitchers.slice(0, 9);\n\n// Step 2: For each of these pitchers, get top 3 batters by OPS\nconst final = [];\n\ntop9.forEach(pitcherName => {\n\tconst matchups = valid.filter(m => m.pitcherName === pitcherName);\n\tconst seenBatters = new Set();\n\tconst topBatters = [];\n\n\tmatchups\n\t\t.sort((a, b) => b.ops - a.ops)\n\t\t.forEach(m => {\n\t\t\tif (!seenBatters.has(m.batterId)) {\n\t\t\t\tseenBatters.add(m.batterId);\n\t\t\t\ttopBatters.push(m);\n\t\t\t}\n\t\t});\n\n\tfinal.push(...topBatters.slice(0, 3));\n});\n\n// Final sort by game start time\nfinal.sort((a, b) => a.gameStartObj - b.gameStartObj);\n\n// Format time and construct output with locked header order\nconst output = final.map(m => {\n\tm.gameStartTime = convertToEasternTime(m.gameStartObj.toISOString());\n\tconst obj = {};\n\tHEADER_ORDER.forEach(key => obj[key] = m[key] ?? '');\n\treturn { json: obj };\n});\n\nreturn output;\n"
},
"notesInFlow": true,
"typeVersion": 2,
"alwaysOutputData": true
},
{
"id": "7d9f19aa-a8a6-4f50-8462-0cad13de6e9c",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-480,
-368
],
"parameters": {
"color": 4,
"width": 2316,
"height": 500,
"content": "Hits"
},
"typeVersion": 1
},
{
"id": "38b3c276-1d1e-4ea5-8427-ddb9f6591bbe",
"name": "Column Order",
"type": "n8n-nodes-base.code",
"position": [
944,
-128
],
"parameters": {
"jsCode": "const orderedKeys = [\n \"gameDate\",\n \"gameStartTime\",\n \"pitcherId\",\n \"pitcherName\",\n \"pitcherTeam\",\n \"pitcherSO\",\n \"pitcherERA\",\n \"pitcherThrows\",\n \"batterBats\",\n \"opponent\",\n \"batterName\",\n \"batterAVG\",\n \"batterHR\",\n \"batterHits\",\n \"batterOPS\",\n \"batterRBI\",\n \"batterId\",\n \"batterPosition\"\n];\n\n// fields that should explicitly be numbers\nconst numericKeys = [\n \"pitcherId\",\n \"pitcherSO\",\n \"pitcherERA\",\n \"batterAVG\",\n \"batterHR\",\n \"batterHits\",\n \"batterOPS\",\n \"batterRBI\",\n \"batterId\"\n];\n\n// \ud83d\udd37 Helper: format to hh:mm AM/PM ET\nfunction toEasternTimeHHMM(value) {\n if (typeof value === 'string' && /^\\d{1,2}:\\d{2}/.test(value)) {\n return value.replace(/:00$/, ''); // clean up trailing :00 if it\u2019s there\n }\n try {\n const utcDate = new Date(value);\n if (isNaN(utcDate.getTime())) return value;\n const options = {\n timeZone: 'America/New_York',\n hour: '2-digit',\n minute: '2-digit',\n hour12: true\n };\n return new Intl.DateTimeFormat('en-US', options).format(utcDate);\n } catch {\n return value;\n }\n}\n\n// Build new list\nconst enriched = items.map(item => {\n const json = { ...item.json };\n\n if (json.gameStartTime) {\n json.gameStartTime = toEasternTimeHHMM(json.gameStartTime);\n }\n\n const output = {};\n for (const key of orderedKeys) {\n let val = json[key] ?? \"\";\n if (numericKeys.includes(key) && val !== \"\") {\n val = Number(val);\n\n // Round batterAVG and batterOPS to .000\n if (key === \"batterAVG\" || key === \"batterOPS\") {\n val = Number(val.toFixed(3));\n }\n }\n output[key] = val;\n }\n\n return { json: output };\n});\n\n// Sort enriched list\nenriched.sort((a, b) => {\n const parseTime = (timeStr) => {\n const [time, meridian] = timeStr.split(' ');\n let [hours, minutes] = time.split(':').map(Number);\n if (meridian === 'PM' && hours !== 12) hours += 12;\n if (meridian === 'AM' && hours === 12) hours = 0;\n return hours * 60 + minutes; // total minutes since midnight\n };\n\n const aTime = parseTime(a.json.gameStartTime);\n const bTime = parseTime(b.json.gameStartTime);\n if (aTime < bTime) return -1;\n if (aTime > bTime) return 1;\n\n const aPitcher = Number(a.json.pitcherId) || 0;\n const bPitcher = Number(b.json.pitcherId) || 0;\n if (aPitcher < bPitcher) return -1;\n if (aPitcher > bPitcher) return 1;\n\n const aOpp = a.json.opponent || \"\";\n const bOpp = b.json.opponent || \"\";\n return aOpp.localeCompare(bOpp);\n});\n\n\nreturn enriched;\n"
},
"typeVersion": 2
},
{
"id": "09b74296-10fa-43a4-8fb4-65a017dd9f78",
"name": "9am Clear",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-400,
-304
],
"parameters": {
"rule": {
"interval": [
{
"triggerAtHour": 9
},
{
"triggerAtHour": 9,
"triggerAtMinute": 15
}
]
}
},
"typeVersion": 1.2
},
{
"id": "31005912-843f-4c2f-8614-c92c00404fb8",
"name": "11:02 - 8:02",
"type": "n8n-nodes-base.scheduleTrigger",
"notes": "02 11-20 * * *",
"position": [
-384,
-128
],
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "02 11-20 * * *"
}
]
}
},
"notesInFlow": true,
"typeVersion": 1
},
{
"id": "e7d4e4e6-09f1-4ce0-820c-a60cd795a858",
"name": "Notes: MLB Hits",
"type": "n8n-nodes-base.stickyNote",
"position": [
288,
-848
],
"parameters": {
"width": 752,
"height": 656,
"content": "MLB \"Hits\" Workflow \u2014 Overview\n\u2022 Pulls today's MLB schedule incl. probablePitcher + lineups (statsapi.mlb.com)\n\u2022 Batches season stats for all involved players\n\u2022 Builds pitcher vs. batter matchup rows\n\u2022 Filters: ERA > 3.33, take top 9 pitchers by ERA, then top 3 opposing batters by OPS each (27 rows)\n\u2022 Sorts by ET start time, writes to Google Sheets (Your Google SHeet \u2192 the tabName)\n\u2022 Sends Telegram message: Top 21 batters by OPS\n\nSetup\n1) Google Sheets OAuth2 \u2192 point to your Google Sheet \u2192 Tab name\n2) Telegram Bot cred + chatId\n3) Optional: tweak ERA/OPS thresholds or Top N in nodes 6 or 8.\n\nTriggers\n\u2022 \"11:02 \u2013 8:02\" runs hourly at :02 (server time)\n\u2022 \"9am Clear\" clears the sheet at 09:00 & 09:15\n\nKey Nodes\n\u2022 2. Get Daily Games \u2014 GET /schedule (date=today; hydrate=probablePitcher,lineups)\n\u2022 3. Extract All Player IDs \u2014 collects personIds (pitchers + lineups)\n\u2022 4. Get Batched Player Stats \u2014 GET /people?personIds=...&hydrate=stats(...)\n\u2022 5. Create Final Matchup Rows \u2014 merges schedule + stats into pitcher\u2194batter rows\n\u2022 6. Filter for Top Matchups \u2014 ERA/OPS filter; pick top 9\u00d73; convert times to ET\n\u2022 Column Order \u2014 enforce column order, numeric typing, rounding (.000 for AVG/OPS)\n\u2022 7. Update n8n-sheet \u2014 append/update by batterId\n\u2022 8. 21 Hitters \u2014 composes Top 21 by OPS message\n\u2022 9. Sends Telegram\n\nNotes\n\u2022 If lineups/schedule not ready yet, downstream may be empty (expected)\n\u2022 Keep node name \"3. Extract All Player IDs\" exact (used by $())\n\u2022 ET conversion is in code; cron uses server time"
},
"typeVersion": 1
},
{
"id": "6ef30ef8-0023-42bc-b322-2fffd650894b",
"name": "8. 21 Hitters",
"type": "n8n-nodes-base.code",
"position": [
1408,
-128
],
"parameters": {
"jsCode": "// This code is for an n8n Code node.\n// It assumes the input from the preceding node (which now provides all 27 batters)\n// allows 'items' to contain all batter records.\n\nconsole.log(\"--- Code Node for Telegram Message Start ---\");\n\n// --- Input Processing (same as before to get all 27 batters) ---\nlet allBatterStats = [];\n\n// This block handles both scenarios:\n// 1. If \"Run Once for All Items\" is ON, 'items' contains all incoming records.\n// 2. If an \"Item Lists (Aggregate)\" node precedes this, 'items[0].json' will be the array.\nfor (const item of items) {\n // Scenario A: Item contains a single object (e.g., from a direct data source outputting individual items)\n if (item && typeof item.json === 'object' && item.json !== null && !Array.isArray(item.json)) {\n allBatterStats.push(item.json);\n }\n // Scenario B: Item.json is an array (e.g., from an Item Lists aggregate node, or if a source directly outputs a single array)\n else if (item && Array.isArray(item.json)) {\n allBatterStats.push(...item.json);\n }\n // Fallback: If the item itself (not its .json) is the object, or other unexpected structures\n else if (typeof item === 'object' && item !== null && item.batterName && item.batterHits) {\n allBatterStats.push(item);\n } else {\n console.warn(\"Skipping an input item with an unrecognized structure:\", JSON.stringify(item, null, 2));\n }\n}\n\nconsole.log(\"Total batter stats collected:\", allBatterStats.length);\n\n// Ensure batterHits is a number and filter out invalid entries.\nconst processedBatterStats = allBatterStats.map(batter => {\n const newBatter = { ...batter };\n newBatter.batterHits = typeof batter.batterHits === 'string'\n ? parseFloat(batter.batterHits)\n : batter.batterHits;\n if (isNaN(newBatter.batterHits)) {\n newBatter.batterHits = 0; // Default invalid numbers to 0\n }\n return newBatter;\n}).filter(batter =>\n typeof batter === 'object' &&\n batter !== null &&\n typeof batter.batterHits === 'number' &&\n typeof batter.batterName === 'string' &&\n batter.batterName.trim() !== ''\n);\n\nconsole.log(\"Number of valid and processed batter stats:\", processedBatterStats.length);\n\nif (processedBatterStats.length === 0) {\n console.warn(\"No valid batter stats found. Returning empty message.\");\n return [{ json: { text: \"No batter stats available to display.\" } }];\n}\n\n// Sort the stats by batterHits in descending order.\nprocessedBatterStats.sort((a, b) => b.batterOPS - a.batterOPS);\n\n// Get the top 21 batters.\nconst top21Batters = processedBatterStats.slice(0, 21);\n\nconsole.log(\"Number of top 21 batters selected:\", top21Batters.length);\n\n// --- Telegram Message Formatting ---\n\n// Start the message with a title. Using Markdown for bold.\nlet telegramMessage = \"\u26be **Top 21 Batters by OPS** \u26be\\n\\n\";\n\n// Add each batter to the message string.\ntop21Batters.forEach((batter, index) => {\n // Format each line using Markdown for name and hits.\n // Remember to escape any special Markdown characters in the data if necessary,\n // though typically names and numbers are safe.\n telegramMessage += `${index + 1}. **${batter.batterName}**: ${batter.batterOPS}\\n`;\n});\n\nconsole.log(\"Generated Telegram message length:\", telegramMessage.length);\n\n// --- Return as a single n8n item for the Telegram node ---\n// The Telegram node's 'Text' field will consume the 'text' property from this output.\nreturn [{\n json: {\n message: telegramMessage // This is the single string containing the entire message\n }\n}];"
},
"executeOnce": false,
"typeVersion": 2,
"alwaysOutputData": true
},
{
"id": "5d726f93-c7f4-42d6-b0a0-c0ff8abc159b",
"name": "Clear your Sheet",
"type": "n8n-nodes-base.googleSheets",
"position": [
-176,
-304
],
"parameters": {
"operation": "clear",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "",
"cachedResultUrl": "",
"cachedResultName": ""
},
"documentId": {
"__rl": true,
"mode": "list",
"value": ""
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.6
},
{
"id": "29d5c57d-4f31-4fcd-aea0-865341d60d9c",
"name": "7. Update Your Sheet",
"type": "n8n-nodes-base.googleSheets",
"onError": "continueErrorOutput",
"position": [
1168,
-128
],
"parameters": {
"operation": "appendOrUpdate",
"sheetName": {
"__rl": true,
"mode": "list",
"value": ""
},
"documentId": {
"__rl": true,
"mode": "list",
"value": ""
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"notesInFlow": true,
"retryOnFail": true,
"typeVersion": 4,
"alwaysOutputData": true,
"waitBetweenTries": 5000
},
{
"id": "e9c4377c-93c9-4286-b567-e233e5e65014",
"name": "9. sendToTelegramChatbot",
"type": "n8n-nodes-base.telegram",
"position": [
1600,
-128
],
"parameters": {
"text": "={{ $json.message }}",
"chatId": "createYourOwnOnTelegram@BOTFather",
"additionalFields": {
"appendAttribution": false
}
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"executeOnce": true,
"typeVersion": 1.2
}
],
"connections": {
"9am Clear": {
"main": [
[
{
"node": "Clear your Sheet",
"type": "main",
"index": 0
}
]
]
},
"11:02 - 8:02": {
"main": [
[
{
"node": "2. Get Daily Games",
"type": "main",
"index": 0
}
]
]
},
"Column Order": {
"main": [
[
{
"node": "7. Update Your Sheet",
"type": "main",
"index": 0
}
]
]
},
"8. 21 Hitters": {
"main": [
[
{
"node": "9. sendToTelegramChatbot",
"type": "main",
"index": 0
}
]
]
},
"2. Get Daily Games": {
"main": [
[
{
"node": "3. Extract All Player IDs",
"type": "main",
"index": 0
}
]
]
},
"7. Update Your Sheet": {
"main": [
[
{
"node": "8. 21 Hitters",
"type": "main",
"index": 0
}
]
]
},
"3. Extract All Player IDs": {
"main": [
[
{
"node": "4. Get Batched Player Stats",
"type": "main",
"index": 0
}
]
]
},
"4. Get Batched Player Stats": {
"main": [
[
{
"node": "5. Create Final Matchup Rows",
"type": "main",
"index": 0
}
]
]
},
"6. Filter for Top Matchups": {
"main": [
[
{
"node": "Column Order",
"type": "main",
"index": 0
}
]
]
},
"5. Create Final Matchup Rows": {
"main": [
[
{
"node": "6. Filter for Top Matchups",
"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.
googleSheetsOAuth2ApitelegramApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
MLB "Hits" Workflow — Overview • Pulls today's MLB schedule incl. probablePitcher + lineups (statsapi.mlb.com) • Batches season stats for all involved players • Builds pitcher vs. batter matchup rows
Source: https://n8n.io/workflows/7252/ — 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 automates plant care reminders and records using Google Sheets, Telegram, and OpenWeather API.
Apollo Data Enrichment Using Company Id to automatically finds contacts for companies listed in your Google Sheet, enriches each person with emails and phone numbers via Apollo’s API, and writes verif
++Download the google sheet here++ and replace this with the googles sheet node: Google sheet , upload to google sheets and replace in the google sheets node. Scheduled trigger: Runs once a day at 8 A
YT AI News Playlist Creator/AI News Form Updater. Uses googleSheets, httpRequest, splitOut, stickyNote. Scheduled trigger; 23 nodes.
Automatically monitor multiple websites every 5 minutes, log downtime, notify your team instantly via multiple channels, and track uptime/downtime in a Google Sheet—without relying on expensive monito