{
  "id": "2wGEVnV2X8031sB8",
  "name": "World Cup 2026 \u2014 Daily Briefing",
  "tags": [],
  "nodes": [
    {
      "id": "8bab0119-cd4b-4817-8f4f-03909d82e7ea",
      "name": "Main Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -208,
        80
      ],
      "parameters": {
        "color": 7,
        "width": 560,
        "height": 1156,
        "content": "## \ud83c\udfc6 World Cup 2026 Daily Briefing\n\nEvery morning this workflow pulls live FIFA World Cup 2026 data, converts all kickoff times to Bangladesh time, asks an AI analyst to write a compact, phone friendly preview of the next day, and posts it straight to Slack.\n\n**Perfect for:** football communities, sports Slack channels, and fan group admins who want an automated daily matchday digest in local time.\n\n***\n\n## How it works\n\n1. **Every Day 07:00 (Asia/Dhaka):** Schedule trigger that fires once daily at 7am Bangladesh time.\n2. **Set Date (BD):** Computes today, yesterday, tomorrow, and a human readable tomorrow date in the Asia/Dhaka timezone.\n3. **Get Games:** Fetches the full match schedule from the worldcup26.ir feed.\n4. **Get Groups:** Fetches group standings tables from the same feed.\n5. **Get Teams:** Fetches the team list with names and FIFA codes.\n6. **Get Stadiums:** Fetches venue and host city details.\n7. **Build Context + Prompt:** Joins all four feeds, converts US venue kickoffs to Bangladesh time, selects tomorrow's fixtures plus recent results, and builds the mobile first AI prompt.\n8. **OpenAI Chat Model1:** GPT 5 Mini model that powers the agent.\n9. **AI Agent:** Generates the formatted matchday briefing (fixtures, win estimates, key players, standings) from the prompt.\n10. **Extract Report Text:** Normalizes the AI output into clean Slack ready text, with a fallback warning if nothing was generated.\n11. **Send to Slack:** Posts the final briefing to your chosen Slack channel as mrkdwn.\n\n***\n\n## Setup (~10 minutes)\n\n1. **OpenAI API:** Add your credential in the *OpenAI Chat Model1* node (model is set to GPT 5 Mini).\n2. **Slack OAuth2:** Connect your Slack account in the *Send to Slack* node and pick the target channel.\n3. **Schedule:** Adjust the cron in *Every Day 07:00 (Asia/Dhaka)* if you want a different delivery time.\n> The match feeds come from the public worldcup26.ir endpoints, so no API key is needed for data. Always run the full workflow from the trigger, because *Build Context + Prompt* reads from every upstream fetch node."
      },
      "typeVersion": 1
    },
    {
      "id": "c0bdaa10-2529-4760-8c21-ae4a6d98bcd2",
      "name": "Section 1 - Trigger & Intake",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        400,
        112
      ],
      "parameters": {
        "color": 5,
        "width": 1400,
        "height": 348,
        "content": "## 1\ufe0f\u20e3 Trigger & Data Intake\n\nThe **Every Day 07:00 (Asia/Dhaka)** schedule trigger starts the run each morning, and **Set Date (BD)** calculates the relevant dates in Bangladesh time. The workflow then fetches all live tournament data in sequence through **Get Games**, **Get Groups**, **Get Teams**, and **Get Stadiums**, pulling fixtures, standings, team names, and venue details from the worldcup26.ir feeds."
      },
      "typeVersion": 1
    },
    {
      "id": "43192b08-ce07-437c-bef5-79bbba560faf",
      "name": "Section 2 - AI Core",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        896,
        480
      ],
      "parameters": {
        "color": 3,
        "width": 540,
        "height": 696,
        "content": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n## 2\ufe0f\u20e3 Prompt Building & AI Analysis\n\n**Build Context + Prompt** merges the four feeds, converts each US venue kickoff into Bangladesh time, and assembles a compact, mobile first prompt covering tomorrow's fixtures, recent results, and standings. The **AI Agent**, powered by the **OpenAI Chat Model1** (GPT 5 Mini), turns that prompt into a skimmable briefing with fixtures, win percentage estimates, key players, and group tables."
      },
      "typeVersion": 1
    },
    {
      "id": "e31b6855-3108-4eb5-972a-ee96b5e0a5e3",
      "name": "Section 3 - Output",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1472,
        480
      ],
      "parameters": {
        "color": 6,
        "width": 568,
        "height": 392,
        "content": "\n\n\n\n\n\n\n\n\n\n## 3\ufe0f\u20e3 Format & Deliver\n\n**Extract Report Text** normalizes the AI Agent output into clean, Slack ready text and adds a fallback message if no briefing was produced. **Send to Slack** then posts the finished daily digest to your chosen channel using Slack mrkdwn formatting."
      },
      "typeVersion": 1
    },
    {
      "id": "f1013ec4-268e-4e16-9200-efc8dd1cb37f",
      "name": "Every Day 07:00 (Asia/Dhaka)",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        528,
        288
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 7 * * *"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "cf0d57db-f0c3-44d6-acb0-bf1d09dfc22c",
      "name": "Set Date (BD)",
      "type": "n8n-nodes-base.set",
      "position": [
        752,
        288
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "a1",
              "name": "date",
              "type": "string",
              "value": "={{ $now.setZone('Asia/Dhaka').toFormat('yyyy-LL-dd') }}"
            },
            {
              "id": "a2",
              "name": "yesterday",
              "type": "string",
              "value": "={{ $now.setZone('Asia/Dhaka').minus({ days: 1 }).toFormat('yyyy-LL-dd') }}"
            },
            {
              "id": "a3",
              "name": "tomorrow",
              "type": "string",
              "value": "={{ $now.setZone('Asia/Dhaka').plus({ days: 1 }).toFormat('yyyy-LL-dd') }}"
            },
            {
              "id": "a4",
              "name": "tomorrowHuman",
              "type": "string",
              "value": "={{ $now.setZone('Asia/Dhaka').plus({ days: 1 }).toFormat('cccc, dd LLLL yyyy') }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "c45e517c-d1cf-4f89-b8f3-6a8a0bcbb5ff",
      "name": "Get Games",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        976,
        288
      ],
      "parameters": {
        "url": "https://worldcup26.ir/get/games",
        "options": {}
      },
      "executeOnce": true,
      "typeVersion": 4.2
    },
    {
      "id": "1a772049-7002-48f7-8f8f-250365eb6234",
      "name": "Get Groups",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1168,
        288
      ],
      "parameters": {
        "url": "https://worldcup26.ir/get/groups",
        "options": {}
      },
      "executeOnce": true,
      "typeVersion": 4.2
    },
    {
      "id": "e1cdd438-5954-4610-be50-d4c44a8e73c4",
      "name": "Get Teams",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1376,
        288
      ],
      "parameters": {
        "url": "https://worldcup26.ir/get/teams",
        "options": {}
      },
      "executeOnce": true,
      "typeVersion": 4.2
    },
    {
      "id": "ccb48fed-e5ea-4a75-9a7f-0e197b2ff9d0",
      "name": "Get Stadiums",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1568,
        288
      ],
      "parameters": {
        "url": "https://worldcup26.ir/get/stadiums",
        "options": {}
      },
      "executeOnce": true,
      "typeVersion": 4.2
    },
    {
      "id": "9d7322df-5b6e-4a87-bb72-c4ae2983b088",
      "name": "Build Context + Prompt",
      "type": "n8n-nodes-base.code",
      "position": [
        976,
        528
      ],
      "parameters": {
        "jsCode": "// Build Context + Prompt \u2014 join worldcup26.ir feeds, convert kickoffs (US venue time) to Bangladesh time, build a COMPACT MOBILE-FIRST next-day prompt.\n// Suffix-proof: auto-resolves duplicated node names (Get Teams, Get Teams1, Get Teams2 ...).\nfunction pickNode(base){\n  for (let s = 0; s <= 5; s++){\n    const nm = s === 0 ? base : base + s;\n    let raw;\n    try { raw = $(nm).all(); } catch (e) { continue; }\n    if (raw && raw.length) return raw.map(i => i.json);\n  }\n  throw new Error('No executed node found for \"' + base + '\". Run the FULL workflow from the trigger (Test workflow), not a single step.');\n}\nfunction unwrap(items){\n  if (items.length === 1){\n    const j = items[0] || {};\n    if (Array.isArray(j)) return j;\n    for (const k of ['data','teams','games','groups','stadiums','result','results']){\n      if (Array.isArray(j[k])) return j[k];\n    }\n  }\n  return items;\n}\n\nlet dWrap;\nfor (let s = 0; s <= 5; s++){\n  const nm = s === 0 ? 'Set Date (BD)' : 'Set Date (BD)' + s;\n  try { const v = $(nm).first(); if (v){ dWrap = v.json; break; } } catch (e) {}\n}\nif (!dWrap) throw new Error('Set Date (BD) node not found/executed. Run the full workflow from the trigger.');\nconst yest = dWrap.yesterday;\nconst schedDate = dWrap.tomorrow;        // preview the NEXT day\nconst schedHuman = dWrap.tomorrowHuman;\n\nconst teams = unwrap(pickNode('Get Teams'));\nconst games = unwrap(pickNode('Get Games'));\nconst groups = unwrap(pickNode('Get Groups'));\nconst stadiums = unwrap(pickNode('Get Stadiums'));\n\nconst teamMap = {};\nfor (const t of teams){ teamMap[String(t.id)] = t; }\nconst stadMap = {};\nfor (const s of stadiums){ stadMap[String(s.id)] = s; }\n\nfunction teamName(id){ const t = teamMap[String(id)]; return t ? (t.name_en || t.name || ('Team '+id)) : ('Team '+id); }\nfunction teamCode(id){ return (teamMap[String(id)] || {}).fifa_code; }\n\n// Host-city -> IANA timezone (16 WC2026 venues across USA / Mexico / Canada)\nfunction tzForCity(city){\n  const c = String(city||'').toLowerCase();\n  if (/inglewood|los angeles|santa clara|san francisco|bay area|seattle/.test(c)) return 'America/Los_Angeles';\n  if (/arlington|dallas|houston|kansas/.test(c)) return 'America/Chicago';\n  if (/atlanta|foxborough|boston|miami|rutherford|new jersey|new york|philadelphia|toronto/.test(c)) return 'America/New_York';\n  if (/monterrey/.test(c)) return 'America/Monterrey';\n  if (/guadalajara|mexico/.test(c)) return 'America/Mexico_City';\n  if (/vancouver/.test(c)) return 'America/Vancouver';\n  return 'America/New_York';\n}\n\nconst enriched = games.map(g => {\n  const st = stadMap[String(g.stadium_id)] || {};\n  const city = st.city_en || st.city || '';\n  const tz = tzForCity(city);\n  const dt = DateTime.fromFormat(String(g.local_date || ''), 'MM/dd/yyyy HH:mm', { zone: tz });\n  const bd = dt.isValid ? dt.setZone('Asia/Dhaka') : null;\n  return {\n    id: g.id,\n    home: g.home_team_name_en || teamName(g.home_team_id),\n    away: g.away_team_name_en || teamName(g.away_team_id),\n    home_code: teamCode(g.home_team_id),\n    away_code: teamCode(g.away_team_id),\n    group: g.group,\n    stage: g.type,\n    venue: st.name_en || ('Stadium ' + g.stadium_id),\n    city: city,\n    bd_time: bd ? bd.toFormat('h:mm a') : null,\n    kickoff_bd: bd ? (bd.toFormat('ccc dd LLL, h:mm a') + ' BDT') : null,\n    kickoff_us_local: dt.isValid ? dt.toFormat('ccc h:mm a ZZZZ') : g.local_date,\n    kickoff_bd_date: bd ? bd.toFormat('yyyy-LL-dd') : null,\n    finished: String(g.finished).toUpperCase() === 'TRUE',\n    score: (g.home_score != null && g.away_score != null) ? (g.home_score + '-' + g.away_score) : null\n  };\n});\n\nconst matches = enriched.filter(g => g.kickoff_bd_date === schedDate);\nconst recentResults = enriched.filter(g => g.kickoff_bd_date === yest && g.finished);\nconst upcomingMatches = enriched\n  .filter(g => g.kickoff_bd_date && g.kickoff_bd_date > schedDate && !g.finished)\n  .sort((a,b) => (a.kickoff_bd_date + a.bd_time).localeCompare(b.kickoff_bd_date + b.bd_time))\n  .slice(0, 8);\n\nconst standings = groups.map(gr => {\n  const rows = (gr.teams || gr.table || []).map(t => {\n    const gf = Number(t.gf), ga = Number(t.ga);\n    return {\n      team: teamName(t.team_id || t.id),\n      P: t.mp, W: t.w, D: t.d, L: t.l,\n      GD: (Number.isFinite(gf) && Number.isFinite(ga)) ? (gf - ga) : t.gd,\n      Pts: t.pts != null ? t.pts : t.points\n    };\n  }).sort((a,b) => {\n    const pa = Number(a.Pts)||0, pb = Number(b.Pts)||0; if (pb !== pa) return pb - pa;\n    return (Number(b.GD)||0) - (Number(a.GD)||0);\n  });\n  return { group: gr.name, table: rows };\n});\n\nconst context = { scheduleDate: schedDate, scheduleHuman: schedHuman, matchCount: matches.length, timezone: 'Asia/Dhaka', matches, recentResults, upcomingMatches, standings };\n\nconst prompt = [\n  'You are a professional football analyst producing a COMPACT, MOBILE-FIRST daily FIFA World Cup 2026 briefing for Slack.',\n  'The reader is in Bangladesh (Asia/Dhaka, UTC+6); the tournament is hosted in the USA, Canada and Mexico. This briefing PREVIEWS the schedule for ' + schedHuman + ' (' + schedDate + ').',\n  '',\n  'CORRECTNESS: the DATA block is LIVE official data (schedule, teams, standings, venues, scores) \u2014 treat it as GROUND TRUTH, never contradict or invent it. bd_time, kickoff_bd and kickoff_us_local are pre-computed \u2014 use them verbatim. Squads, player clubs and win probabilities are YOUR analysis (label win % as estimates); only name players/clubs you are confident about for the 2025/26 season.',\n  '',\n  'MOBILE FORMAT (CRITICAL): most readers use Slack on a PHONE. Do NOT use triple-backtick code blocks, tables, or pipe (|) alignment \u2014 they get cut off and wrap badly on small screens. Use ONLY short bullet lines. Slack mrkdwn: *bold*, _italic_, bullet \"\u2022\". Keep every line short.',\n  '',\n  'Produce these sections IN THIS ORDER, nothing extra:',\n  '1) Bold title line: \ud83c\udfc6 WC2026 \u2014 <schedule weekday + short date e.g. Mon 15 Jun> \u2022 <matchCount> matches \u2022 times BDT',\n  '2) If recentResults is non-empty, ONE italic line: _Last: CODE x-y CODE \u2022 CODE x-y CODE_ (FIFA codes + score).',\n  '3) *\u26bd Fixtures* then, per match in matches, TWO short lines: line one \"\u2022 <bd_time> \u2014 <HOME_CODE> v <AWAY_CODE> (<Grp>)\"; line two indented \"    <stadium>, <city> \u2022 <kickoff_us_local>\". Use FIFA codes. If matches is empty, write \"\u2022 No matches \u2014 next up:\" then the next few upcomingMatches the same way.',\n  '4) *\ud83d\udd2e Win % (H/D/A est.)* then ONE line per match: \"\u2022 <HOME_CODE> v <AWAY_CODE> \u2014 H/D/A\" as plain numbers, e.g. \"\u2022 NED v JPN \u2014 55/25/20\" (each set sums about 100).',\n  '5) *\u2b50 Key players* then ONE line per team in matches: \"\u2022 CODE: Player (Club), Player (Club)\" \u2014 max 2 stars each, only when confident; skip a team if unsure.',\n  '6) *\ud83d\udcca Standings (P/GD/Pts)* then, for each group appearing in matches: a \"*Group X*\" line, then one line per team \"\u2022 Team \u2014 P/GD/Pts\" sorted as given. If matches is empty, show all 12 groups this way.',\n  '7) Final italic line: _Have a great matchday!_ \u26bd',\n  '',\n  'Keep the whole message tight and skimmable on a phone. Absolutely no code blocks, no tables, no pipe alignment.',\n  '',\n  'DATA:',\n  JSON.stringify(context)\n].join('\\n');\n\nreturn [{ json: { scheduleDate: schedDate, scheduleHuman: schedHuman, prompt, context } }];\n"
      },
      "executeOnce": true,
      "typeVersion": 2
    },
    {
      "id": "ef873e21-d4d0-4924-8a13-102e4ee9bc09",
      "name": "AI Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        1232,
        528
      ],
      "parameters": {
        "text": "={{ $json.prompt }}",
        "options": {},
        "promptType": "define"
      },
      "typeVersion": 1.7
    },
    {
      "id": "263afb6d-aa6f-4f88-9214-0f2b7f4ebba0",
      "name": "Extract Report Text",
      "type": "n8n-nodes-base.code",
      "position": [
        1520,
        528
      ],
      "parameters": {
        "jsCode": "// Normalize the report text from an AI Agent node (output) OR the Claude Messages API (content[]).\nconst data = $input.first().json || {};\nlet report = '';\nif (typeof data.output === 'string') {\n  report = data.output.trim();                       // n8n AI Agent / LangChain node\n} else if (typeof data.text === 'string') {\n  report = data.text.trim();                          // Basic LLM Chain node\n} else if (Array.isArray(data.content)) {\n  report = data.content.filter(b => b.type === 'text').map(b => b.text).join('\\n').trim(); // raw Claude API\n} else if (data.message && typeof data.message.content === 'string') {\n  report = data.message.content.trim();\n}\nif (!report) {\n  report = '\u26a0\ufe0f No World Cup briefing was generated today. Check the AI Agent output and upstream feeds.';\n}\nreturn [{ json: { report } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "844b7830-d1b6-4444-9400-c1a59a151a4b",
      "name": "Send to Slack",
      "type": "n8n-nodes-base.slack",
      "position": [
        1712,
        528
      ],
      "parameters": {
        "text": "={{ $json.report }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "C0BAJGKUQ04",
          "cachedResultName": "worldcup-2026"
        },
        "otherOptions": {
          "mrkdwn": true
        },
        "authentication": "oAuth2"
      },
      "credentials": {
        "slackOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "80dc115e-429c-4ca5-8fc7-9d8f3de23f0c",
      "name": "OpenAI Chat Model1",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        1248,
        752
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-5-mini"
        },
        "options": {},
        "builtInTools": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "2dfefc21-1e18-4e49-94b1-efbbebf184a5",
  "nodeGroups": [],
  "connections": {
    "AI Agent": {
      "main": [
        [
          {
            "node": "Extract Report Text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Games": {
      "main": [
        [
          {
            "node": "Get Groups",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Teams": {
      "main": [
        [
          {
            "node": "Get Stadiums",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Groups": {
      "main": [
        [
          {
            "node": "Get Teams",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Stadiums": {
      "main": [
        [
          {
            "node": "Build Context + Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Date (BD)": {
      "main": [
        [
          {
            "node": "Get Games",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model1": {
      "ai_languageModel": [
        [
          {
            "node": "AI Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Extract Report Text": {
      "main": [
        [
          {
            "node": "Send to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Context + Prompt": {
      "main": [
        [
          {
            "node": "AI Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Every Day 07:00 (Asia/Dhaka)": {
      "main": [
        [
          {
            "node": "Set Date (BD)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}