This workflow corresponds to n8n.io template #14368 — 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 →
{
"id": "OmdrhvIGnaod6pOS",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Real-Time IPL Commentary Generator With Live API Data and GPT-4o Narratives",
"tags": [],
"nodes": [
{
"id": "99d686e3-4136-4977-8d78-dd2a1ee2dfa0",
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-96,
224
],
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "*/6 14-23 * * *"
}
]
}
},
"typeVersion": 1.3
},
{
"id": "0fd36fb5-fa86-44a9-8937-f7bdad51e668",
"name": "Manual test trigger",
"type": "n8n-nodes-base.webhook",
"position": [
-80,
448
],
"parameters": {
"path": "ipl-narrative",
"options": {},
"responseMode": "responseNode"
},
"typeVersion": 2.1
},
{
"id": "f1e1634c-04d0-4fe0-95e8-324eb63fba15",
"name": "Fetch live match list",
"type": "n8n-nodes-base.httpRequest",
"position": [
320,
304
],
"parameters": {
"url": "https://api.cricapi.com/v1/currentMatches",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "apikey",
"value": "your_api_key"
},
{
"name": "offset",
"value": "0"
}
]
}
},
"typeVersion": 4.3
},
{
"id": "e0437979-34e0-49cc-980d-343bbcb7543e",
"name": "Narrative Generator",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
1008,
288
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "gpt-4o-mini",
"cachedResultName": "GPT-4O-MINI"
},
"options": {},
"responses": {
"values": [
{
"content": "={{ $json.prompt }}"
},
{
"role": "system",
"content": "You are an expert IPL cricket analyst. You write short punchy win-probability narratives for fan apps. Always be specific about numbers. Never use percentages. Always write exactly 2 sentences."
}
]
},
"builtInTools": {}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.1
},
{
"id": "fb301e1f-0c26-4ebc-be1c-485e6e8b327d",
"name": "Compute Match Indicators",
"type": "n8n-nodes-base.code",
"position": [
528,
304
],
"parameters": {
"jsCode": "const response = $input.first().json;\n\nif (response.status !== \"success\") {\n throw new Error(`CricAPI error: ${response.status}`);\n}\n\nconst matches = response.data || [];\n\n// Find live IPL match dynamically\nconst iplMatch = matches.find(m => {\n const name = (m.name || '').toUpperCase();\n const isIPL = name.includes('IPL') || name.includes('INDIAN PREMIER LEAGUE');\n const isT20 = (m.matchType || '').toLowerCase() === 't20';\n const isLive = m.matchStarted === true && m.matchEnded === false;\n return isIPL && isT20 && isLive;\n});\n\n// Fallback to any live T20 for testing when IPL is not on\nconst match = iplMatch || matches.find(m =>\n (m.matchType || '').toLowerCase() === 't20' &&\n m.matchStarted === true &&\n m.matchEnded === false &&\n m.score && m.score.length > 0\n);\n\n// Further fallback \u2014 use any T20 with scores for demo purposes\nconst demoMatch = match || matches.find(m =>\n (m.matchType || '').toLowerCase() === 't20' &&\n m.score && m.score.length > 0\n);\n\nif (!demoMatch) {\n return [{ json: { skip: true, reason: 'No T20 matches available' } }];\n}\n\nconst isLiveIPL = !!iplMatch;\nconst scores = demoMatch.score || [];\nconst innings = scores.length >= 2 ? '2nd' : '1st';\n\nlet narrative_data = {};\n\nif (innings === '1st') {\n const batting = scores[0] || {};\n const runs = batting.r || 0;\n const wickets = batting.w || 0;\n const overs = parseFloat(batting.o) || 0;\n const oversRemaining = Math.max(0, 20 - overs);\n const crr = overs > 0 ? (runs / overs).toFixed(2) : '0.00';\n const projectedTotal = Math.round(runs + (parseFloat(crr) * oversRemaining));\n\n let phase = 'Middle overs';\n if (overs <= 6) phase = 'Powerplay';\n else if (overs >= 15) phase = 'Death overs';\n\n let pressure = 'Medium';\n if (wickets >= 6) pressure = 'High';\n else if (wickets <= 2 && overs <= 10) pressure = 'Low';\n\n narrative_data = {\n innings: '1st',\n battingTeam: demoMatch.teams?.[0] || 'Team A',\n bowlingTeam: demoMatch.teams?.[1] || 'Team B',\n score: `${runs}/${wickets}`,\n overs,\n oversRemaining: oversRemaining.toFixed(1),\n crr,\n projectedTotal,\n wicketsInHand: 10 - wickets,\n phase,\n pressure,\n target: null,\n rrr: null,\n runsNeeded: null\n };\n\n} else {\n const firstInnings = scores[0] || {};\n const secondInnings = scores[1] || {};\n const target = (firstInnings.r || 0) + 1;\n const runs = secondInnings.r || 0;\n const wickets = secondInnings.w || 0;\n const overs = parseFloat(secondInnings.o) || 0;\n const oversRemaining = Math.max(0, 20 - overs);\n const runsNeeded = target - runs;\n const crr = overs > 0 ? (runs / overs).toFixed(2) : '0.00';\n const rrr = oversRemaining > 0 ? (runsNeeded / oversRemaining).toFixed(2) : '99.00';\n const rrrGap = (parseFloat(rrr) - parseFloat(crr)).toFixed(2);\n\n let phase = 'Middle overs';\n if (overs <= 6) phase = 'Powerplay';\n else if (overs >= 15) phase = 'Death overs';\n\n let pressure = 'Medium';\n if (parseFloat(rrr) > 12 || wickets >= 7) pressure = 'High';\n else if (parseFloat(rrr) < 8 && wickets <= 4) pressure = 'Low';\n\n narrative_data = {\n innings: '2nd',\n battingTeam: demoMatch.teams?.[1] || 'Team B',\n bowlingTeam: demoMatch.teams?.[0] || 'Team A',\n score: `${runs}/${wickets}`,\n overs,\n oversRemaining: oversRemaining.toFixed(1),\n target,\n runsNeeded,\n crr,\n rrr,\n rrrGap,\n wicketsInHand: 10 - wickets,\n phase,\n pressure,\n projectedTotal: null\n };\n}\n\n// Build OpenAI prompt based on innings\nlet prompt = '';\n\nif (narrative_data.innings === '1st') {\n prompt = `You are an IPL cricket commentator writing a punchy 2-sentence win-probability narrative for a fan app.\n\nMatch: ${narrative_data.battingTeam} vs ${narrative_data.bowlingTeam}\nInnings: 1st innings\nPhase: ${narrative_data.phase}\nScore: ${narrative_data.score} in ${narrative_data.overs} overs\nCurrent Run Rate: ${narrative_data.crr}\nWickets in Hand: ${narrative_data.wicketsInHand}\nOvers Remaining: ${narrative_data.oversRemaining}\nProjected Total: ${narrative_data.projectedTotal}\nPressure Level: ${narrative_data.pressure}\n\nWrite exactly 2 sentences. Be specific about the numbers provided. Sound like a human analyst not a robot. Use cricket language fans understand. No percentages allowed.`;\n\n} else {\n prompt = `You are an IPL cricket commentator writing a punchy 2-sentence win-probability narrative for a fan app.\n\nMatch: ${narrative_data.battingTeam} chasing ${narrative_data.target} vs ${narrative_data.bowlingTeam}\nInnings: 2nd innings (chase)\nPhase: ${narrative_data.phase}\nScore: ${narrative_data.score} in ${narrative_data.overs} overs\nRuns Needed: ${narrative_data.runsNeeded} off ${narrative_data.oversRemaining} overs\nRequired Run Rate: ${narrative_data.rrr}\nCurrent Run Rate: ${narrative_data.crr}\nRun Rate Gap: ${narrative_data.rrrGap}\nWickets in Hand: ${narrative_data.wicketsInHand}\nPressure Level: ${narrative_data.pressure}\n\nWrite exactly 2 sentences. Be specific about the numbers provided. Sound like a human analyst not a robot. Use cricket language fans understand. No percentages allowed.`;\n}\n\nreturn [{\n json: {\n ...narrative_data,\n matchName: demoMatch.name,\n isLiveIPL,\n prompt,\n timestamp: new Date().toISOString()\n }\n}];"
},
"typeVersion": 2
},
{
"id": "28927d49-279f-4bb2-98dd-bd97c78b8942",
"name": "Parse Narrative",
"type": "n8n-nodes-base.code",
"position": [
1360,
288
],
"parameters": {
"jsCode": "const response = $input.first().json;\n\n// Correct path for n8n OpenAI node output\nconst narrative = response.output?.[0]?.content?.[0]?.text || 'Narrative unavailable.';\n\nconst data = $('Compute Match Indicators').first().json;\n\nreturn [{\n json: {\n ...data,\n narrative\n }\n}];"
},
"typeVersion": 2
},
{
"id": "c0eb01b1-facc-4e55-84da-fd996880a1be",
"name": "Respond to Webhook",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
1776,
288
],
"parameters": {
"options": {},
"respondWith": "json",
"responseBody": "={\n \"match\": \"{{ $json.match_name }}\",\n \"innings\": \"{{ $json.innings }}\",\n \"score\": \"{{ $json.score }}\",\n \"over\": \"{{ $json.over }}\",\n \"phase\": \"{{ $json.phase }}\",\n \"pressure\": \"{{ $json.pressure }}\",\n \"narrative\": \"{{ $json.narrative }}\",\n \"timestamp\": \"{{ $json.timestamp }}\"\n}"
},
"typeVersion": 1.5
},
{
"id": "056f77d8-5c13-4219-a2db-24498169ba02",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-976,
-144
],
"parameters": {
"width": 432,
"height": 592,
"content": "## Workflow Overview\n\n### How it works\n\nThis workflow turns live IPL match data into \nhuman-sounding win-probability narratives that \nupdate every 6 minutes throughout a match. It \nfetches live scores from CricAPI, computes key \ncricket indicators like required run rate, current \nrun rate, wickets in hand, match phase, and pressure \nlevel, then sends all of that context to GPT-4o \nwhich generates a 2-sentence analyst-style narrative \nready to embed in any fan app, widget, or dashboard.\n\n### Setup Steps\n\n1. Sign up at cricapi.com and get your API key\n2. Go to Settings \u2192 Variables and add CRICAPI_KEY\n3. Add your OpenAI API key to the HTTP Request node\n4. Create the Google Sheet with columns listed below\n5. Connect your Google Sheets OAuth2 credentials\n6. Activate the workflow \u2014 it runs automatically \n every 6 minutes during match hours (2PM\u201311PM)\n7. Test anytime via the webhook URL\n\n"
},
"typeVersion": 1
},
{
"id": "7fcf17b7-9101-4fb8-a2c0-add2b9214939",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-256,
-16
],
"parameters": {
"color": 7,
"width": 480,
"height": 656,
"content": "Two triggers run this workflow. The Schedule Trigger fires automatically every 6 minutes between 2PM and 11PM covering all IPL match windows. The Webhook Trigger lets you fire it manually at any time for \ntesting or on-demand narrative generation without waiting for the schedule. Both triggers feed into the same API call so the logic is identical."
},
"typeVersion": 1
},
{
"id": "2718ebd0-1172-44a8-b581-6d14b884a18b",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
256,
176
],
"parameters": {
"color": 7,
"width": 640,
"height": 336,
"content": "One API call to CricAPI fetches all current matches. The Code node filters for a live IPL match first \u2014 if IPL is not in season it falls back to any live T20 match for testing. It then detects which innings is active and computes all indicators \u2014 CRR, RRR, runs needed, overs remaining, wickets in hand, match phase, and pressure level. Both 1st and 2nd innings are handled with different prompt logic per scenario."
},
"typeVersion": 1
},
{
"id": "bb963c19-5fe5-42be-a292-d5cfd8c3ef7c",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
912,
160
],
"parameters": {
"color": 7,
"width": 608,
"height": 336,
"content": "The computed indicators are assembled into a structured prompt and sent to GPT-4o via HTTP Request. The system prompt instructs the model to write exactly 2 sentences, use specific numbers, avoid percentages, and sound like a human cricket \nanalyst. The response is parsed and passed to both the Google Sheets log and the Webhook response node so any app polling the webhook gets the narrative instantly."
},
"typeVersion": 1
},
{
"id": "81e1a1c4-63df-4f14-ad92-507061325a24",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
1536,
112
],
"parameters": {
"color": 7,
"width": 496,
"height": 368,
"content": "Every narrative is logged with timestamp, match name, innings, over, score, target, RRR, CRR, phase, pressure, and the full narrative text. This creates a complete ball-by-ball record of every narrative generated across the match. \n\nWebhook Response \u2014 returns a clean JSON payload that any \nfan app, widget, or dashboard can poll to display \nthe latest narrative in real time."
},
"typeVersion": 1
},
{
"id": "bedb3220-f1de-4716-b741-181a3f691715",
"name": "Log to IPL Win Probability Log",
"type": "n8n-nodes-base.googleSheets",
"position": [
1568,
288
],
"parameters": {
"columns": {
"value": {
"crr": "={{ $json.crr }}",
"rrr": "={{ $json.rrr }}",
"over": "={{ $json.overs }}",
"phase": "={{ $json.phase }}",
"score": "={{ $json.score }}",
"target": "={{ $json.target }}",
"innings": "={{ $json.innings }}",
"pressure": "={{ $json.pressure }}",
"narrative": "={{ $json.narrative }}",
"timestamp": "={{ $json.timestamp }}",
"match_name": "={{ $json.matchName }}"
},
"schema": [
{
"id": "timestamp",
"type": "string",
"display": true,
"required": false,
"displayName": "timestamp",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "match_name",
"type": "string",
"display": true,
"required": false,
"displayName": "match_name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "innings",
"type": "string",
"display": true,
"required": false,
"displayName": "innings",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "over",
"type": "string",
"display": true,
"required": false,
"displayName": "over",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "score",
"type": "string",
"display": true,
"required": false,
"displayName": "score",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "target",
"type": "string",
"display": true,
"required": false,
"displayName": "target",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "rrr",
"type": "string",
"display": true,
"required": false,
"displayName": "rrr",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "crr",
"type": "string",
"display": true,
"required": false,
"displayName": "crr",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "phase",
"type": "string",
"display": true,
"required": false,
"displayName": "phase",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "pressure",
"type": "string",
"display": true,
"required": false,
"displayName": "pressure",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "narrative",
"type": "string",
"display": true,
"required": false,
"displayName": "narrative",
"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/1HQAGEGnogUsFXb7bVpkIRfAKHQRlwsZiq1Jx2pj14PI/edit#gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1HQAGEGnogUsFXb7bVpkIRfAKHQRlwsZiq1Jx2pj14PI",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1HQAGEGnogUsFXb7bVpkIRfAKHQRlwsZiq1Jx2pj14PI/edit?usp=drivesdk",
"cachedResultName": "IPL Win Probability Log"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "0b2a28ae-31c0-4c0d-9f28-5ba82a7170ab",
"name": "Is Live Match ?",
"type": "n8n-nodes-base.if",
"position": [
736,
304
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "f77887cd-660c-4e9d-abd2-6318eca74270",
"operator": {
"type": "string",
"operation": "notEquals"
},
"leftValue": "={{ $json.skip }}",
"rightValue": "true"
}
]
},
"looseTypeValidation": true
},
"typeVersion": 2.3
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "5e69937b-de9b-48cc-9d2f-cc9e4a6d1869",
"connections": {
"Is Live Match ?": {
"main": [
[
{
"node": "Narrative Generator",
"type": "main",
"index": 0
}
]
]
},
"Parse Narrative": {
"main": [
[
{
"node": "Log to IPL Win Probability Log",
"type": "main",
"index": 0
}
]
]
},
"Schedule Trigger": {
"main": [
[
{
"node": "Fetch live match list",
"type": "main",
"index": 0
}
]
]
},
"Manual test trigger": {
"main": [
[
{
"node": "Fetch live match list",
"type": "main",
"index": 0
}
]
]
},
"Narrative Generator": {
"main": [
[
{
"node": "Parse Narrative",
"type": "main",
"index": 0
}
]
]
},
"Fetch live match list": {
"main": [
[
{
"node": "Compute Match Indicators",
"type": "main",
"index": 0
}
]
]
},
"Compute Match Indicators": {
"main": [
[
{
"node": "Is Live Match ?",
"type": "main",
"index": 0
}
]
]
},
"Log to IPL Win Probability Log": {
"main": [
[
{
"node": "Respond to Webhook",
"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.
googleSheetsOAuth2ApiopenAiApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
IPL fans expect more than a scoreboard. They want to know what the score means — is the batting team ahead of the game, is the chase getting away from them, who has the pressure right now. This workflow answers all of that automatically by fetching live IPL match data every 6…
Source: https://n8n.io/workflows/14368/ — 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
This automation is a complete end-to-end system designed to find, qualify, and contact B2B leads — fully automated and powered by AI. Searches for target companies on LinkedIn via Ghost Genius API, us
This comprehensive n8n automation template orchestrates a complete end-to-end workflow for generating engaging short-form Point-of-View (POV) style videos using multiple AI services and automatically
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 signal