AutomationFlowsGeneral › Convert Utc to Mena Local Time with Hijri Seasons and Ramadan Send Rules

Convert Utc to Mena Local Time with Hijri Seasons and Ramadan Send Rules

ByIbrahim @ibmabr on n8n.io

This sub-workflow takes a UTC timestamp and MENA locale inputs and returns a typed object with local time, workweek/weekend status, Hijri (Umm al-Qura) season flags, and Ramadan-aware sendability rules including a suggested next send time. Receives inputs from another workflow…

Event trigger★★★★☆ complexity15 nodesExecute Workflow Trigger
General Trigger: Event Nodes: 15 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #17880 — we link there as the canonical source.

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 →

Download .json
{
  "id": "h6lHBu07HwpxuhAK",
  "meta": {
    "builderVariant": "mcp",
    "aiBuilderAssisted": true
  },
  "name": "Convert UTC timestamps to MENA local time, work week and Ramadan-aware send windows",
  "tags": [],
  "nodes": [
    {
      "id": "a80d7e80-4b11-4f2a-8ac8-dea3ec087aad",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1664,
        480
      ],
      "parameters": {
        "width": 480,
        "height": 1184,
        "content": "## Convert UTC timestamps to MENA local time, work week and Ramadan-aware send windows\n\nEvery n8n workspace runs on UTC, and almost every scheduling template assumes a Mon-Fri week. Both are wrong for the Arabic-speaking world: the work week is Sun-Thu, the consumer peak is 19:00-22:00 local, and Ramadan moves that peak past midnight.\n\nCall this sub-workflow from any automation with a timestamp and a country. It returns one typed object telling you what time it really is there.\n\n### How it works\n\nThree Code nodes. No packages, no credentials, no API calls.\n\n1. **Local time and work week** - converts to the target zone, emits an explicit +03:00 offset, and maps the weekday against a per-country weekend table. Also returns `local_day_start_utc` and `local_day_end_utc`, the values your daily rollups should use instead of `CURRENT_DATE`. That is the fix for numbers that appear to reset at 03:00.\n2. **Hijri and season** - Umm al-Qura date from Node's built-in ICU, plus Ramadan, Eid al-Fitr, Eid al-Adha and Hajj flags.\n3. **Daypart and send window** - returns `is_sendable`, and when false a `block_reason` and a `send_after` timestamp you can queue on. Friday 11:00-13:00 is suppressed for everyone.\n\n### Setup\n\nNothing to configure to try it. Open **Initiate Self Test** and execute: 20 assertions run and print a pass/fail table.\n\nTo use it, add an Execute Sub-workflow node in your own workflow and pass `ts_utc`, `tz`, `country` (iq sa ae kw qa bh om jo eg) and `audience` (b2c or b2b).\n\n### Customization\n\nEvery country defaults to a Fri-Sat weekend except `ae`, which is Sat-Sun for the public sector since 2022. Edit the `WEEKEND` map in the first and third Code nodes if your market differs.\n\nPass `holiday_overrides` as `[{ date: \"YYYY-MM-DD\", country: \"iq\", name: \"Eid\" }]` to correct dates. Use `\"*\"` for all countries. This matters: Umm al-Qura is astronomical, while real Eid dates follow local moon sighting and can differ by a day between neighbouring countries."
      },
      "typeVersion": 1
    },
    {
      "id": "4017752e-4eda-4526-9d35-573f2dccce1f",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2224,
        480
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 368,
        "content": "## Production entry path\n\nA calling workflow passes ts_utc, tz, country, audience and optional holiday_overrides. Anything missing falls back to Baghdad, Iraq, b2c and now."
      },
      "typeVersion": 1
    },
    {
      "id": "e060ba6a-e228-41cb-8832-65ae486a986a",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2224,
        880
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 336,
        "content": "## Self-test entry path\n\nBuilds 20 assertion cases and feeds them through the same resolver the production path uses. Run it manually after import."
      },
      "typeVersion": 1
    },
    {
      "id": "fac2bfec-81b3-414e-b55b-ed271b7cb59a",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2672,
        608
      ],
      "parameters": {
        "color": 7,
        "width": 640,
        "height": 336,
        "content": "## Resolve time context\n\nThree pure-JS stages shared by both entry paths: local time and Sun-Thu work week, then Umm al-Qura Hijri and season, then daypart and send window."
      },
      "typeVersion": 1
    },
    {
      "id": "cfd5fd69-4e77-453f-9d9b-572bbf5cd807",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3344,
        480
      ],
      "parameters": {
        "color": 7,
        "width": 480,
        "height": 560,
        "content": "## Route the output\n\nSelf-test runs get a pass/fail table. Real callers get the typed contract, where every timestamp carries an explicit UTC offset, never a naive local time."
      },
      "typeVersion": 1
    },
    {
      "id": "75fd20ed-46ed-4379-9b51-5252dc070404",
      "name": "Workflow Invocation Trigger",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "position": [
        2272,
        688
      ],
      "parameters": {
        "workflowInputs": {
          "values": [
            {
              "name": "ts_utc"
            },
            {
              "name": "tz"
            },
            {
              "name": "country"
            },
            {
              "name": "audience"
            },
            {
              "name": "holiday_overrides",
              "type": "array"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "3645514b-5787-4e41-bad0-e82bc3d96ab4",
      "name": "Set Default Parameters",
      "type": "n8n-nodes-base.set",
      "position": [
        2496,
        688
      ],
      "parameters": {
        "options": {
          "ignoreConversionErrors": true
        },
        "assignments": {
          "assignments": [
            {
              "id": "a1",
              "name": "tz",
              "type": "string",
              "value": "={{ $json.tz || \"Asia/Baghdad\" }}"
            },
            {
              "id": "a2",
              "name": "country",
              "type": "string",
              "value": "={{ $json.country || \"iq\" }}"
            },
            {
              "id": "a3",
              "name": "audience",
              "type": "string",
              "value": "={{ $json.audience || \"b2c\" }}"
            },
            {
              "id": "a4",
              "name": "ts_utc",
              "type": "string",
              "value": "={{ $json.ts_utc || $now.toUTC().toISO() }}"
            },
            {
              "id": "a5",
              "name": "holiday_overrides",
              "type": "array",
              "value": "={{ $json.holiday_overrides || [] }}"
            },
            {
              "id": "a6",
              "name": "is_self_test",
              "type": "boolean",
              "value": false
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.5
    },
    {
      "id": "a4d8fb5e-7c72-45ff-bffa-ef1854e9ad74",
      "name": "Calculate Local Time & Work Week",
      "type": "n8n-nodes-base.code",
      "position": [
        2720,
        784
      ],
      "parameters": {
        "jsCode": "\nconst WD = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };\n\nconst WEEKEND = {\n  iq: [5, 6], sa: [5, 6], kw: [5, 6], qa: [5, 6], bh: [5, 6],\n  om: [5, 6], jo: [5, 6], eg: [5, 6],\n  ae: [6, 0]\n};\n\nfunction tzParts(date, tz) {\n  const dtf = new Intl.DateTimeFormat('en-US', {\n    timeZone: tz, hour12: false, weekday: 'short',\n    year: 'numeric', month: '2-digit', day: '2-digit',\n    hour: '2-digit', minute: '2-digit', second: '2-digit'\n  });\n  const o = {};\n  for (const p of dtf.formatToParts(date)) o[p.type] = p.value;\n  if (o.hour === '24') o.hour = '00';\n  return o;\n}\n\nfunction offsetMinutes(date, tz) {\n  const p = tzParts(date, tz);\n  const asUTC = Date.UTC(+p.year, +p.month - 1, +p.day, +p.hour, +p.minute, +p.second);\n  return Math.round((asUTC - date.getTime()) / 60000);\n}\n\nfunction offsetString(mins) {\n  const sign = mins < 0 ? '-' : '+';\n  const a = Math.abs(mins);\n  return sign + String(Math.floor(a / 60)).padStart(2, '0') + ':' + String(a % 60).padStart(2, '0');\n}\n\nfunction utcFromWall(y, mo, d, h, mi, s, tz) {\n  const target = Date.UTC(y, mo - 1, d, h, mi, s);\n  let guess = target;\n  for (let i = 0; i < 4; i++) {\n    const off = offsetMinutes(new Date(guess), tz);\n    const next = target - off * 60000;\n    if (next === guess) break;\n    guess = next;\n  }\n  return new Date(guess);\n}\n\nconst out = [];\nfor (const item of $input.all()) {\n  const j = item.json;\n  const tz = j.tz || 'Asia/Baghdad';\n  const country = String(j.country || 'iq').toLowerCase();\n  const audience = j.audience === 'b2b' ? 'b2b' : 'b2c';\n\n  const base = j.ts_utc ? new Date(j.ts_utc) : new Date();\n  if (isNaN(base.getTime())) {\n    throw new Error('MENA Clock: invalid ts_utc \"' + j.ts_utc + '\" (expected ISO 8601)');\n  }\n\n  const p = tzParts(base, tz);\n  const offMin = offsetMinutes(base, tz);\n  const offStr = offsetString(offMin);\n  const wdIdx = WD[p.weekday];\n  const weekend = WEEKEND[country] || [5, 6];\n  const isWeekend = weekend.indexOf(wdIdx) !== -1;\n\n  const y = +p.year, mo = +p.month, d = +p.day;\n  const dayStart = utcFromWall(y, mo, d, 0, 0, 0, tz);\n  const probe = tzParts(new Date(dayStart.getTime() + 26 * 3600000), tz);\n  const dayEnd = utcFromWall(+probe.year, +probe.month, +probe.day, 0, 0, 0, tz);\n\n  let nextWork = null;\n  for (let k = 0; k <= 8; k++) {\n    const cand = new Date(dayStart.getTime() + k * 90000000);\n    const cp = tzParts(cand, tz);\n    if (weekend.indexOf(WD[cp.weekday]) !== -1) continue;\n    const start = utcFromWall(+cp.year, +cp.month, +cp.day, 9, 0, 0, tz);\n    if (start.getTime() > base.getTime()) { nextWork = start; break; }\n  }\n\n  out.push({ json: Object.assign({}, j, {\n    tz: tz,\n    country: country,\n    audience: audience,\n    ts_utc: base.toISOString(),\n    local_iso: p.year + '-' + p.month + '-' + p.day + 'T' + p.hour + ':' + p.minute + ':' + p.second + offStr,\n    local_date: p.year + '-' + p.month + '-' + p.day,\n    local_hour: +p.hour,\n    local_minute: +p.minute,\n    utc_offset: offStr,\n    utc_offset_minutes: offMin,\n    weekday_local: p.weekday,\n    weekday_index: wdIdx,\n    is_weekend: isWeekend,\n    is_workday: !isWeekend,\n    next_workday_start: nextWork ? nextWork.toISOString() : null,\n    local_day_start_utc: dayStart.toISOString(),\n    local_day_end_utc: dayEnd.toISOString()\n  }) });\n}\nreturn out;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "d00e0e9e-9777-4542-9042-513df9c0a5d4",
      "name": "Determine Hijri Date & Season",
      "type": "n8n-nodes-base.code",
      "position": [
        2944,
        784
      ],
      "parameters": {
        "jsCode": "\nconst HIJRI_MONTHS = [\n  'Muharram', 'Safar', 'Rabi al-Awwal', 'Rabi al-Thani', 'Jumada al-Ula', 'Jumada al-Akhirah',\n  'Rajab', 'Shaban', 'Ramadan', 'Shawwal', 'Dhu al-Qadah', 'Dhu al-Hijjah'\n];\n\nfunction hijriOn(date, tz) {\n  const dtf = new Intl.DateTimeFormat('en-u-ca-islamic-umalqura', {\n    timeZone: tz, year: 'numeric', month: 'numeric', day: 'numeric'\n  });\n  const o = {};\n  for (const p of dtf.formatToParts(date)) o[p.type] = p.value;\n  return { y: parseInt(o.year, 10), m: parseInt(o.month, 10), d: parseInt(o.day, 10) };\n}\n\nconst out = [];\nfor (const item of $input.all()) {\n  const j = item.json;\n  const tz = j.tz;\n  const base = new Date(j.ts_utc);\n  const h = hijriOn(base, tz);\n\n  const isRamadan = h.m === 9;\n  const isEidFitr = h.m === 10 && h.d >= 1 && h.d <= 3;\n  const isEidAdha = h.m === 12 && h.d >= 10 && h.d <= 13;\n  const isHajj = h.m === 12 && h.d >= 8 && h.d <= 13;\n\n  let daysToRamadan = 0;\n  if (!isRamadan) {\n    daysToRamadan = null;\n    for (let k = 1; k <= 380; k++) {\n      const probe = hijriOn(new Date(base.getTime() + k * 86400000), tz);\n      if (probe.m === 9) { daysToRamadan = k; break; }\n    }\n  }\n\n  const overrides = Array.isArray(j.holiday_overrides) ? j.holiday_overrides : [];\n  let holiday = null;\n  for (const o of overrides) {\n    if (!o || !o.date) continue;\n    const scope = String(o.country || '*').toLowerCase();\n    if (scope !== '*' && scope !== j.country) continue;\n    if (String(o.date) === j.local_date) { holiday = String(o.name || 'Holiday'); break; }\n  }\n\n  out.push({ json: Object.assign({}, j, {\n    hijri_year: h.y,\n    hijri_month: h.m,\n    hijri_day: h.d,\n    hijri_month_name: HIJRI_MONTHS[h.m - 1] || String(h.m),\n    is_ramadan: isRamadan,\n    ramadan_day: isRamadan ? h.d : null,\n    is_eid_fitr: isEidFitr,\n    is_eid_adha: isEidAdha,\n    is_hajj_window: isHajj,\n    days_to_ramadan: daysToRamadan,\n    is_holiday: holiday !== null,\n    holiday_name: holiday\n  }) });\n}\nreturn out;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "8f2c6886-20d3-4102-9a35-9a009bdb27f5",
      "name": "Identify Daypart & Send Window",
      "type": "n8n-nodes-base.code",
      "position": [
        3168,
        784
      ],
      "parameters": {
        "jsCode": "\nconst WD = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };\nconst WEEKEND = {\n  iq: [5, 6], sa: [5, 6], kw: [5, 6], qa: [5, 6], bh: [5, 6],\n  om: [5, 6], jo: [5, 6], eg: [5, 6],\n  ae: [6, 0]\n};\n\nfunction tzParts(date, tz) {\n  const dtf = new Intl.DateTimeFormat('en-US', {\n    timeZone: tz, hour12: false, weekday: 'short',\n    year: 'numeric', month: '2-digit', day: '2-digit',\n    hour: '2-digit', minute: '2-digit', second: '2-digit'\n  });\n  const o = {};\n  for (const p of dtf.formatToParts(date)) o[p.type] = p.value;\n  if (o.hour === '24') o.hour = '00';\n  return o;\n}\nfunction offsetMinutes(date, tz) {\n  const p = tzParts(date, tz);\n  return Math.round((Date.UTC(+p.year, +p.month - 1, +p.day, +p.hour, +p.minute, +p.second) - date.getTime()) / 60000);\n}\nfunction utcFromWall(y, mo, d, h, mi, s, tz) {\n  const target = Date.UTC(y, mo - 1, d, h, mi, s);\n  let guess = target;\n  for (let i = 0; i < 4; i++) {\n    const off = offsetMinutes(new Date(guess), tz);\n    const next = target - off * 60000;\n    if (next === guess) break;\n    guess = next;\n  }\n  return new Date(guess);\n}\n\nconst SEND_START = 9;\nconst PRAYER_START = 11;\nconst PRAYER_END = 13;\nconst RAMADAN_LATE_END = 2;\n\nfunction daypartFor(h, audience, isRamadan) {\n  if (audience === 'b2b') {\n    if (h >= 9 && h < 14) return 'business_peak';\n    if (h >= 6 && h < 9) return 'morning';\n    if (h >= 14 && h < 18) return 'afternoon';\n    if (h >= 18 && h < 23) return 'evening';\n    return 'late_night';\n  }\n  if (isRamadan) {\n    if (h >= 22 || h < 2) return 'evening_peak';\n    if (h >= 2 && h < 6) return 'late_night';\n    if (h >= 6 && h < 12) return 'morning';\n    if (h >= 12 && h < 17) return 'afternoon';\n    return 'evening';\n  }\n  if (h >= 19 && h < 22) return 'evening_peak';\n  if (h >= 6 && h < 12) return 'morning';\n  if (h >= 12 && h < 17) return 'afternoon';\n  if (h >= 17 && h < 19) return 'evening';\n  if (h >= 22) return 'evening';\n  return 'late_night';\n}\n\nconst out = [];\nfor (const item of $input.all()) {\n  const j = item.json;\n  const tz = j.tz;\n  const audience = j.audience;\n  const isB2B = audience === 'b2b';\n  const weekend = WEEKEND[j.country] || [5, 6];\n  const base = new Date(j.ts_utc);\n  const h = j.local_hour;\n\n  const isRamadanB2C = !isB2B && j.is_ramadan;\n  const sendEnd = isB2B ? 17 : (isRamadanB2C ? 24 : 21);\n  const lateWindowEnd = isRamadanB2C ? RAMADAN_LATE_END : 0;\n\n  const daypart = daypartFor(h, audience, j.is_ramadan);\n\n  const inWindow = (h >= SEND_START && h < sendEnd) || (lateWindowEnd > 0 && h < lateWindowEnd);\n\n  let blockReason = null;\n  if (isB2B && j.is_weekend) blockReason = 'weekend';\n  else if (isB2B && j.is_holiday) blockReason = 'holiday';\n  else if (j.weekday_index === 5 && h >= PRAYER_START && h < PRAYER_END) blockReason = 'friday_prayer';\n  else if (!inWindow) blockReason = h < SEND_START ? 'before_window' : 'after_window';\n\n  const isSendable = blockReason === null;\n\n  let sendAfter = null;\n  if (!isSendable) {\n    if (blockReason === 'friday_prayer') {\n      const p = tzParts(base, tz);\n      sendAfter = utcFromWall(+p.year, +p.month, +p.day, PRAYER_END, 0, 0, tz);\n    } else {\n      const dayStart = new Date(j.local_day_start_utc);\n      for (let k = 0; k <= 9; k++) {\n        const cp = tzParts(new Date(dayStart.getTime() + k * 90000000), tz);\n        if (isB2B && weekend.indexOf(WD[cp.weekday]) !== -1) continue;\n        const cand = utcFromWall(+cp.year, +cp.month, +cp.day, SEND_START, 0, 0, tz);\n        if (cand.getTime() > base.getTime()) { sendAfter = cand; break; }\n      }\n    }\n  }\n\n  out.push({ json: Object.assign({}, j, {\n    daypart: daypart,\n    is_evening_peak: daypart === 'evening_peak',\n    is_business_peak: daypart === 'business_peak',\n    send_window_start_hour: SEND_START,\n    send_window_end_hour: sendEnd,\n    is_sendable: isSendable,\n    block_reason: blockReason,\n    send_after: sendAfter ? sendAfter.toISOString() : null\n  }) });\n}\nreturn out;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "185fe995-5743-4446-ad20-06c17955c982",
      "name": "Check Self Test Condition",
      "type": "n8n-nodes-base.if",
      "position": [
        3392,
        784
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.is_self_test }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "b3df067d-9473-4a3e-915c-a62e2de8e83f",
      "name": "Perform Assertions & Reporting",
      "type": "n8n-nodes-base.code",
      "position": [
        3616,
        880
      ],
      "parameters": {
        "jsCode": "\nconst rows = [];\nlet passed = 0, failed = 0;\n\nfor (const item of $input.all()) {\n  const j = item.json;\n  const expect = j.expect || {};\n  const misses = [];\n  for (const key of Object.keys(expect)) {\n    const want = expect[key];\n    const got = j[key];\n    if (JSON.stringify(want) !== JSON.stringify(got)) {\n      misses.push(key + ': expected ' + JSON.stringify(want) + ', got ' + JSON.stringify(got));\n    }\n  }\n  const ok = misses.length === 0;\n  if (ok) passed++; else failed++;\n  rows.push({\n    status: ok ? 'PASS' : 'FAIL',\n    case: j.case_name,\n    local_iso: j.local_iso,\n    failures: misses\n  });\n}\n\nreturn [{ json: {\n  suite: 'MENA Clock self-test',\n  total: rows.length,\n  passed: passed,\n  failed: failed,\n  all_passed: failed === 0,\n  results: rows\n} }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "79a008aa-da07-45ea-b69c-73b0ad30cbec",
      "name": "Return MENA Clock Contract",
      "type": "n8n-nodes-base.set",
      "position": [
        3616,
        688
      ],
      "parameters": {
        "options": {
          "ignoreConversionErrors": true
        },
        "assignments": {
          "assignments": [
            {
              "id": "o1",
              "name": "local_iso",
              "type": "string",
              "value": "={{ $json.local_iso }}"
            },
            {
              "id": "o2",
              "name": "local_date",
              "type": "string",
              "value": "={{ $json.local_date }}"
            },
            {
              "id": "o3",
              "name": "utc_offset",
              "type": "string",
              "value": "={{ $json.utc_offset }}"
            },
            {
              "id": "o4",
              "name": "weekday_local",
              "type": "string",
              "value": "={{ $json.weekday_local }}"
            },
            {
              "id": "o5",
              "name": "is_workday",
              "type": "boolean",
              "value": "={{ $json.is_workday }}"
            },
            {
              "id": "o6",
              "name": "is_weekend",
              "type": "boolean",
              "value": "={{ $json.is_weekend }}"
            },
            {
              "id": "o7",
              "name": "next_workday_start",
              "type": "string",
              "value": "={{ $json.next_workday_start }}"
            },
            {
              "id": "o8",
              "name": "local_day_start_utc",
              "type": "string",
              "value": "={{ $json.local_day_start_utc }}"
            },
            {
              "id": "o9",
              "name": "local_day_end_utc",
              "type": "string",
              "value": "={{ $json.local_day_end_utc }}"
            },
            {
              "id": "o10",
              "name": "hijri_year",
              "type": "number",
              "value": "={{ $json.hijri_year }}"
            },
            {
              "id": "o11",
              "name": "hijri_month_name",
              "type": "string",
              "value": "={{ $json.hijri_month_name }}"
            },
            {
              "id": "o12",
              "name": "hijri_day",
              "type": "number",
              "value": "={{ $json.hijri_day }}"
            },
            {
              "id": "o13",
              "name": "is_ramadan",
              "type": "boolean",
              "value": "={{ $json.is_ramadan }}"
            },
            {
              "id": "o14",
              "name": "ramadan_day",
              "type": "number",
              "value": "={{ $json.ramadan_day }}"
            },
            {
              "id": "o15",
              "name": "is_eid_fitr",
              "type": "boolean",
              "value": "={{ $json.is_eid_fitr }}"
            },
            {
              "id": "o16",
              "name": "is_eid_adha",
              "type": "boolean",
              "value": "={{ $json.is_eid_adha }}"
            },
            {
              "id": "o17",
              "name": "is_hajj_window",
              "type": "boolean",
              "value": "={{ $json.is_hajj_window }}"
            },
            {
              "id": "o18",
              "name": "days_to_ramadan",
              "type": "number",
              "value": "={{ $json.days_to_ramadan }}"
            },
            {
              "id": "o19",
              "name": "is_holiday",
              "type": "boolean",
              "value": "={{ $json.is_holiday }}"
            },
            {
              "id": "o20",
              "name": "holiday_name",
              "type": "string",
              "value": "={{ $json.holiday_name }}"
            },
            {
              "id": "o21",
              "name": "daypart",
              "type": "string",
              "value": "={{ $json.daypart }}"
            },
            {
              "id": "o22",
              "name": "is_evening_peak",
              "type": "boolean",
              "value": "={{ $json.is_evening_peak }}"
            },
            {
              "id": "o23",
              "name": "is_sendable",
              "type": "boolean",
              "value": "={{ $json.is_sendable }}"
            },
            {
              "id": "o24",
              "name": "block_reason",
              "type": "string",
              "value": "={{ $json.block_reason }}"
            },
            {
              "id": "o25",
              "name": "send_after",
              "type": "string",
              "value": "={{ $json.send_after }}"
            }
          ]
        }
      },
      "typeVersion": 3.5
    },
    {
      "id": "82f8e648-92bb-4553-903c-2ea97e5f479b",
      "name": "Initiate Self Test",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        2272,
        1056
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "f08bd719-3ad1-4f91-a622-b5326bfe827c",
      "name": "Build Assertion Cases",
      "type": "n8n-nodes-base.code",
      "position": [
        2496,
        1056
      ],
      "parameters": {
        "jsCode": "\nconst cases = [\n  { name: 'Baghdad Friday is a weekend day', ts_utc: '2026-08-07T09:00:00Z', tz: 'Asia/Baghdad', country: 'iq', audience: 'b2b',\n    expect: { weekday_local: 'Fri', is_weekend: true, is_workday: false, is_sendable: false, block_reason: 'weekend', utc_offset: '+03:00' } },\n  { name: 'Baghdad Sunday is a workday', ts_utc: '2026-08-09T07:00:00Z', tz: 'Asia/Baghdad', country: 'iq', audience: 'b2b',\n    expect: { weekday_local: 'Sun', is_weekend: false, is_workday: true, is_sendable: true, block_reason: null } },\n  { name: 'UAE Sunday is a WEEKEND day (Sat-Sun since 2022)', ts_utc: '2026-08-09T07:00:00Z', tz: 'Asia/Dubai', country: 'ae', audience: 'b2b',\n    expect: { is_weekend: true, is_workday: false, utc_offset: '+04:00' } },\n  { name: 'Baghdad 22:00 local resolves the correct local day start', ts_utc: '2026-08-09T19:00:00Z', tz: 'Asia/Baghdad', country: 'iq', audience: 'b2c',\n    expect: { local_hour: 22, local_date: '2026-08-09', local_day_start_utc: '2026-08-08T21:00:00.000Z' } },\n  { name: 'Baghdad 00:30 local belongs to the NEXT local day, not the UTC day', ts_utc: '2026-08-09T21:30:00Z', tz: 'Asia/Baghdad', country: 'iq', audience: 'b2c',\n    expect: { local_date: '2026-08-10', local_hour: 0, is_sendable: false, block_reason: 'before_window' } },\n  { name: 'b2c evening peak is 19:00-22:00 outside Ramadan', ts_utc: '2026-08-09T17:00:00Z', tz: 'Asia/Baghdad', country: 'iq', audience: 'b2c',\n    expect: { local_hour: 20, daypart: 'evening_peak', is_sendable: true } },\n  { name: 'Friday midday prayer window is blocked', ts_utc: '2026-08-07T09:30:00Z', tz: 'Asia/Baghdad', country: 'iq', audience: 'b2c',\n    expect: { local_hour: 12, weekday_local: 'Fri', is_sendable: false, block_reason: 'friday_prayer' } },\n  { name: 'A non-Ramadan date reports is_ramadan false', ts_utc: '2026-08-09T12:00:00Z', tz: 'Asia/Baghdad', country: 'iq', audience: 'b2c',\n    expect: { is_ramadan: false, ramadan_day: null } },\n  { name: 'Civil override marks a holiday and blocks b2b', ts_utc: '2026-08-10T08:00:00Z', tz: 'Asia/Baghdad', country: 'iq', audience: 'b2b',\n    holiday_overrides: [{ date: '2026-08-10', country: 'iq', name: 'Test Holiday' }],\n    expect: { is_holiday: true, holiday_name: 'Test Holiday', is_sendable: false, block_reason: 'holiday' } },\n  { name: 'Override scoped to another country does not apply', ts_utc: '2026-08-10T08:00:00Z', tz: 'Asia/Baghdad', country: 'iq', audience: 'b2b',\n    holiday_overrides: [{ date: '2026-08-10', country: 'sa', name: 'Saudi Only' }],\n    expect: { is_holiday: false, holiday_name: null } },\n  { name: 'Wildcard override applies to every country', ts_utc: '2026-08-10T08:00:00Z', tz: 'Asia/Baghdad', country: 'iq', audience: 'b2b',\n    holiday_overrides: [{ date: '2026-08-10', country: '*', name: 'Global Day' }],\n    expect: { is_holiday: true, holiday_name: 'Global Day' } },\n  { name: 'Cairo resolves its own local weekday', ts_utc: '2026-08-09T10:00:00Z', tz: 'Africa/Cairo', country: 'eg', audience: 'b2c',\n    expect: { weekday_local: 'Sun' } },\n  { name: 'Before the window queues to 09:00 the same day', ts_utc: '2026-08-09T04:00:00Z', tz: 'Asia/Baghdad', country: 'iq', audience: 'b2c',\n    expect: { local_hour: 7, is_sendable: false, block_reason: 'before_window', send_after: '2026-08-09T06:00:00.000Z' } },\n  { name: 'After the b2c window queues to 09:00 the next day', ts_utc: '2026-08-09T19:30:00Z', tz: 'Asia/Baghdad', country: 'iq', audience: 'b2c',\n    expect: { local_hour: 22, is_sendable: false, block_reason: 'after_window', send_after: '2026-08-10T06:00:00.000Z' } },\n  { name: 'b2b Thursday evening queues to Sunday 09:00, skipping Fri-Sat', ts_utc: '2026-08-06T15:00:00Z', tz: 'Asia/Baghdad', country: 'iq', audience: 'b2b',\n    expect: { weekday_local: 'Thu', is_sendable: false, send_after: '2026-08-09T06:00:00.000Z' } },\n  { name: 'next_workday_start skips the Fri-Sat weekend', ts_utc: '2026-08-07T09:00:00Z', tz: 'Asia/Baghdad', country: 'iq', audience: 'b2b',\n    expect: { next_workday_start: '2026-08-09T06:00:00.000Z' } },\n  { name: 'Ramadan 22:30 b2c is the peak AND is sendable, not cut off at 21:00', ts_utc: '2026-02-25T19:30:00Z', tz: 'Asia/Baghdad', country: 'iq', audience: 'b2c',\n    expect: { is_ramadan: true, ramadan_day: 8, local_hour: 22, daypart: 'evening_peak', is_sendable: true, block_reason: null } },\n  { name: 'Ramadan 01:30 b2c is still inside the late window, past midnight', ts_utc: '2026-02-25T22:30:00Z', tz: 'Asia/Baghdad', country: 'iq', audience: 'b2c',\n    expect: { is_ramadan: true, local_hour: 1, is_sendable: true, block_reason: null } },\n  { name: 'Ramadan 03:00 b2c is past the late window and blocked', ts_utc: '2026-02-26T00:00:00Z', tz: 'Asia/Baghdad', country: 'iq', audience: 'b2c',\n    expect: { is_ramadan: true, local_hour: 3, is_sendable: false, block_reason: 'before_window' } },\n  { name: 'Ramadan does NOT extend the b2b window', ts_utc: '2026-02-25T19:30:00Z', tz: 'Asia/Baghdad', country: 'iq', audience: 'b2b',\n    expect: { is_ramadan: true, local_hour: 22, is_sendable: false, block_reason: 'after_window' } }\n];\n\nreturn cases.map(function (c) {\n  return { json: {\n    is_self_test: true,\n    case_name: c.name,\n    expect: c.expect,\n    ts_utc: c.ts_utc,\n    tz: c.tz,\n    country: c.country,\n    audience: c.audience,\n    holiday_overrides: c.holiday_overrides || []\n  } };\n});\n"
      },
      "typeVersion": 2
    }
  ],
  "active": true,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": true,
    "executionOrder": "v1"
  },
  "versionId": "37868551-daa4-4a8f-8e08-79eca82c1bfa",
  "nodeGroups": [],
  "connections": {
    "Initiate Self Test": {
      "main": [
        [
          {
            "node": "Build Assertion Cases",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Assertion Cases": {
      "main": [
        [
          {
            "node": "Calculate Local Time & Work Week",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Default Parameters": {
      "main": [
        [
          {
            "node": "Calculate Local Time & Work Week",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Self Test Condition": {
      "main": [
        [
          {
            "node": "Perform Assertions & Reporting",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Return MENA Clock Contract",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Workflow Invocation Trigger": {
      "main": [
        [
          {
            "node": "Set Default Parameters",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Determine Hijri Date & Season": {
      "main": [
        [
          {
            "node": "Identify Daypart & Send Window",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Identify Daypart & Send Window": {
      "main": [
        [
          {
            "node": "Check Self Test Condition",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Local Time & Work Week": {
      "main": [
        [
          {
            "node": "Determine Hijri Date & Season",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This sub-workflow takes a UTC timestamp and MENA locale inputs and returns a typed object with local time, workweek/weekend status, Hijri (Umm al-Qura) season flags, and Ramadan-aware sendability rules including a suggested next send time. Receives inputs from another workflow…

Source: https://n8n.io/workflows/17880/ — original creator credit. Request a take-down →

More General workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

General

Back up n8n workflows to Google Drive automatically This workflow provides a robust solution for backing up your n8n workflows to Google Drive. It is designed to handle backups for multiple n8n instan

Execute Workflow Trigger, n8n, Google Drive
General

This workflow uses KlickTipp community nodes, available for self-hosted n8n instances only.

Execute Workflow Trigger, N8N Nodes Klicktipp
General

This workflow is a robust and forgiving JSON parser designed to handle malformed or "dirty" JSON strings often returned by AI models or scraped from web pages. It takes a text string as input and atte

Execute Workflow Trigger
General

This automation monitors X (formerly Twitter) search pages in real time and extracts high-signal posts that match your categories of interest. It’s ideal for community engagement, lead discovery, thou

Execute Workflow Trigger, Airtop
General

This template provides a powerful and configurable utility to convert JSON data into a clean, well-structured XML format. It is designed for developers, data analysts, and n8n users who need to interf

Execute Workflow Trigger