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 →
{
"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
}
]
]
}
}
}
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 →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
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
This workflow uses KlickTipp community nodes, available for self-hosted n8n instances only.
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
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
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