{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "3973284d-8c68-42c3-81ee-9de605e47f22",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -80,
        -112
      ],
      "parameters": {
        "color": 7,
        "width": 400,
        "height": 550,
        "content": "## Brent Crude Futures Tracker\n\nSilently scrapes live Brent Crude futures prices for the next 10 months, formats them into a clean table, and delivers updates to Telegram.\n\n### How it works\n1. Triggers multiple times a day on weekdays\n2. Scrapes oilprice.com Brent Crude page\n3. Parses 10 forward contracts (prices and changes)\n4. Aggregates data into a single array\n5. Formats and sends an HTML table to Telegram\n\n### Setup\n- [ ] Add your Telegram Bot credentials\n- [ ] Enter your Telegram Chat ID in the `Send Telegram Table` node\n- [ ] Adjust the `Market Hours Trigger` node times if you are not in IST timezone\n\n### How to Use\nSimply wait for the scheduled times, or click 'Test Workflow' to receive a live snapshot of the forward curve instantly."
      },
      "typeVersion": 1
    },
    {
      "id": "125f8d81-c5c9-4152-a8d9-5b51f8a85445",
      "name": "Sec1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        384,
        48
      ],
      "parameters": {
        "width": 412,
        "height": 244,
        "content": "## 1. Schedule & Fetch\nRuns on schedule and gets raw HTML."
      },
      "typeVersion": 1
    },
    {
      "id": "6afbdff3-ec6a-4b20-8371-3d0e6e5cb3cc",
      "name": "Sec2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        816,
        48
      ],
      "parameters": {
        "width": 420,
        "height": 244,
        "content": "## 2. Parse & Combine\nExtracts 10 contracts and bundles them."
      },
      "typeVersion": 1
    },
    {
      "id": "72e21ada-fd5a-44e0-be5f-dac28813893a",
      "name": "Sec3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1280,
        48
      ],
      "parameters": {
        "width": 250,
        "height": 244,
        "content": "## 3. Notify\nSends HTML formatted table to Telegram."
      },
      "typeVersion": 1
    },
    {
      "id": "8959692d-f633-40c8-b2f8-799570b5b165",
      "name": "Market Hours Trigger1",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        448,
        144
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [
                1,
                2,
                3,
                4,
                5
              ],
              "triggerAtHour": 9
            },
            {
              "field": "weeks",
              "triggerAtDay": [
                1,
                2,
                3,
                4,
                5
              ],
              "triggerAtHour": 11,
              "triggerAtMinute": 11
            },
            {
              "field": "weeks",
              "triggerAtDay": [
                1,
                2,
                3,
                4,
                5
              ],
              "triggerAtHour": 13,
              "triggerAtMinute": 30
            },
            {
              "field": "weeks",
              "triggerAtDay": [
                1,
                2,
                3,
                4,
                5
              ],
              "triggerAtHour": 16
            },
            {
              "field": "weeks",
              "triggerAtDay": [
                1,
                2,
                3,
                4,
                5
              ],
              "triggerAtHour": 19,
              "triggerAtMinute": 15
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "9b4bd061-ca85-4279-9163-f165ad1ed7e5",
      "name": "Fetch Brent Page1",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        640,
        144
      ],
      "parameters": {
        "url": "https://oilprice.com/futures/brent/",
        "options": {}
      },
      "typeVersion": 4.3
    },
    {
      "id": "8a60254d-4ad8-4fa3-8d90-cd4df1b68b94",
      "name": "Parse 10 Contracts1",
      "type": "n8n-nodes-base.code",
      "onError": "continueRegularOutput",
      "position": [
        896,
        144
      ],
      "parameters": {
        "jsCode": "// CORRECTED Code - Extracts correct \"Last\" price column\nconst htmlContent = $input.item.json.data || \"\";\n\nif (!htmlContent || typeof htmlContent !== 'string') {\n  return [{ json: { error: \"No HTML content\" } }];\n}\n\nconst now = new Date();\nconst scrapeDate = now.toISOString().split('T')[0];\nconst scrapeTime = now.toTimeString().split(' ')[0];\n\nconst symbolsData = [\n  { symbol: 'CBH26', month: 'Mar' },\n  { symbol: 'CBJ26', month: 'Apr' },\n  { symbol: 'CBK26', month: 'May' },\n  { symbol: 'CBM26', month: 'Jun' },\n  { symbol: 'CBN26', month: 'Jul' },\n  { symbol: 'CBQ26', month: 'Aug' },\n  { symbol: 'CBU26', month: 'Sep' },\n  { symbol: 'CBV26', month: 'Oct' },\n  { symbol: 'CBX26', month: 'Nov' },\n  { symbol: 'CBZ26', month: 'Dec' }\n];\n\nconst results = [];\n\nfor (const { symbol, month } of symbolsData) {\n  const idx = htmlContent.indexOf(symbol);\n  \n  if (idx > -1) {\n    // Get context AFTER the symbol (not before)\n    // This ensures we get data from the same row\n    const ctx = htmlContent.substring(idx, Math.min(htmlContent.length, idx + 800));\n    \n    // Look for the \"Last\" column value specifically\n    // Pattern: symbol, then month/year, then the FIRST number is Last price\n    const priceMatch = ctx.match(/CBH26[\\s\\S]{0,200}?<\\/a>[\\s\\S]{0,200}?<div[^>]*>\\s*([0-9]{2}\\.[0-9]{2})\\s*</);\n    \n    // Alternative: Use more specific pattern for \"last\" column\n    const lastPriceMatch = ctx.match(/<div[^>]*class=\"[^\"]*last[^\"]*\"[^>]*>\\s*([0-9]{2}\\.[0-9]{2})/i);\n    \n    // Try another pattern - look for first price after contract name\n    const firstPriceMatch = ctx.match(/2026[^0-9]*([0-9]{2}\\.[0-9]{2})/);\n    \n    const price = lastPriceMatch ? lastPriceMatch[1] : \n                  (priceMatch ? priceMatch[1] : \n                  (firstPriceMatch ? firstPriceMatch[1] : null));\n    \n    if (price) {\n      // Find change value\n      const changeMatch = ctx.match(/([+\\-][0-9]\\.[0-9]{2})/);\n      \n      // Find time\n      const timeMatch = ctx.match(/([0-9]{2}:[0-9]{2})/);\n      \n      results.push({\n        json: {\n          symbol: symbol,\n          month: month,\n          year: \"2026\",\n          contract: `${month} 2026`,\n          price_usd: parseFloat(price),\n          change: changeMatch ? parseFloat(changeMatch[1]) : null,\n          website_update_time: timeMatch ? timeMatch[1] : \"\",\n          scrape_date: scrapeDate,\n          scrape_time: scrapeTime,\n          source: \"https://oilprice.com/futures/brent/\",\n          commodity: \"Brent Crude Oil Futures\"\n        }\n      });\n    }\n  }\n}\n\nif (results.length === 0) {\n  return [{\n    json: {\n      error: \"No contracts extracted\",\n      scrape_date: scrapeDate\n    }\n  }];\n}\n\nreturn results;\n"
      },
      "typeVersion": 2,
      "alwaysOutputData": true
    },
    {
      "id": "2f39141f-73cf-4163-9295-6d7522ed30f5",
      "name": "Aggregate Items1",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        1088,
        144
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData"
      },
      "typeVersion": 1
    },
    {
      "id": "8467a8ed-7938-46d7-b890-738ac6288fc0",
      "name": "Send Telegram Table1",
      "type": "n8n-nodes-base.telegram",
      "position": [
        1360,
        144
      ],
      "parameters": {
        "text": "=\ud83d\udee2\ufe0f <b>Brent Crude Oil Futures</b>\n\ud83d\udcc5 {{ $json.data[0].scrape_date }} \u2022 {{ $json.data[0].scrape_time }}\n\n{{ $json.data.every(item => item.change >= 0) ? '\u2705 ALL UP' : $json.data.every(item => item.change < 0) ? '\u26a0\ufe0f ALL DOWN' : '\ud83d\udcca MIXED' }}\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n\n<pre>\nContract  Period    Price   Change\n\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\n{{ $json.data.map(item => {\n  const emoji = item.change > 0 ? '\ud83d\udfe2' : item.change < 0 ? '\ud83d\udd34' : '\u26aa';\n  const symbol = item.symbol.padEnd(9);\n  const period = `${item.month} '${String(item.year).slice(-2)}`.padEnd(9);\n  const price = `$${item.price_usd}`.padEnd(7);\n  const change = `${item.change >= 0 ? '+' : ''}$${item.change}`;\n  return `${emoji} ${symbol} ${period} ${price} ${change}`;\n}).join('\\n') }}\n</pre>\n\n\ud83d\udcca Range: ${{ Math.min(...$json.data.map(d => d.price_usd)) }} - ${{ Math.max(...$json.data.map(d => d.price_usd)) }}\n\ud83d\udcc8 Avg: {{ ($json.data.reduce((sum, d) => sum + d.change, 0) / $json.data.length).toFixed(2) >= 0 ? '+' : '' }}${{ ($json.data.reduce((sum, d) => sum + d.change, 0) / $json.data.length).toFixed(2) }}\n\n\ud83d\udd17 <a href=\"{{ $json.data[0].source }}\">Source</a>",
        "chatId": "123456789",
        "additionalFields": {
          "parse_mode": "HTML",
          "appendAttribution": false
        }
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    }
  ],
  "connections": {
    "Aggregate Items1": {
      "main": [
        [
          {
            "node": "Send Telegram Table1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Brent Page1": {
      "main": [
        [
          {
            "node": "Parse 10 Contracts1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse 10 Contracts1": {
      "main": [
        [
          {
            "node": "Aggregate Items1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Market Hours Trigger1": {
      "main": [
        [
          {
            "node": "Fetch Brent Page1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}