إعداد المناطق الزمنية ومشغّل الجدولة في n8n
يتعامل n8n المستضاف ذاتياً مع 3 إعدادات للمنطقة الزمنية. تعرّف على سبب ظهور الخطأ في التوقيت واضبط TZ وGENERIC_TIMEZONE ومنطقة سير العمل.
لماذا يعمل مشغّل الجدولة في n8n في ساعة غير صحيحة
يعمل مشغّل الجدولة في n8n في ساعة غير صحيحة لأن n8n يقرأ المنطقة الزمنية من ثلاثة مواضع منفصلة، وتصحيح أحدها يحل جزءاً فقط من المشكلة. هذه المواضع الثلاثة هي متغير TZ الخاص بالحاوية، والإعداد الافتراضي للمثيل GENERIC_TIMEZONE، والمنطقة الزمنية المحددة داخل سير عمل فردي. اضبط المواضع الثلاثة مرة واحدة، وستعمل كل جدولة تنشئها بعد ذلك في الوقت المتوقع.
صحّح أولاً افتراضاً واحداً. لا يجدول n8n المستضاف ذاتياً حديث الإنشاء وفق UTC (التوقيت العالمي المنسق). تكون ساعة الحاوية مضبوطة على UTC لأن الصورة الرسمية لا تحدد TZ. الجدولة طبقة منفصلة، والقيمة الافتراضية الموثقة في n8n لـ GENERIC_TIMEZONE هي America/New_York (حتى August 2026)، ولذلك يشغّل المثيل غير المعدّل Schedule Triggers وفق توقيت New York. لهذا نادراً ما يطابق الفارق الزمني الذي يبلّغ عنه المستخدمون بُعدهم الفعلي عن UTC. فمالك في Berlin يطلب التشغيل عند 06:00، لكنه يحصل عليه عند 12:00 بالتوقيت المحلي، وعند 11:00 خلال الأسابيع الواقعة في March التي تكون فيها الولايات المتحدة قد انتقلت إلى التوقيت الصيفي بينما لم تنتقل إليه Europe بعد.
طبقات المناطق الزمنية الثلاث، وأيّها تكون لها الأولوية
TZ هي المنطقة الزمنية لنظام التشغيل داخل الحاوية. توضّح وثائق n8n أنها المتغيّر الذي يضبط المنطقة الزمنية للنظام، للتحكم في ما تُرجعه نصوص مثل date وأوامرها. وهي تحدد ما يطبعه date داخل الحاوية، والطابع الزمني الذي يظهر في سطر سجل الحاوية، وما تُرجعه new Date() في عقدة Code، وما يراه أيّ نص shell تشغّله داخلها. ولا تؤثر في وقت تشغيل Schedule Trigger.
GENERIC_TIMEZONE هي المنطقة الزمنية لمثيل n8n. تسمّيها الوثائق المنطقة الزمنية لمثيل n8n، وتوضح أنها مهمة لعقد الجدولة مثل Cron. ويشير Cron هنا إلى الصيغة القياسية للجدولة المستندة إلى الوقت، ويعرضها n8n في Schedule Trigger بوصفها الخيار Custom (Cron).
تُضبط المنطقة الزمنية لسير العمل لكل سير عمل على حدة. افتح سير العمل في لوحة الرسم، وحدد النقاط الثلاث في الزاوية العلوية اليمنى، ثم حدد Settings، وغيّر قيمة Timezone. يؤدي ذلك إلى تجاوز GENERIC_TIMEZONE في سير العمل نفسه.
يكون ترتيب الأولوية ثابتاً في Schedule Trigger. يستخدم n8n المنطقة الزمنية لسير العمل إذا كانت محددة، وإلا يستخدم المنطقة الزمنية للمثيل من GENERIC_TIMEZONE، وإلا يستخدم الإعداد الافتراضي المضمّن لديه وهو America/New_York. ولا يُستعلم عن TZ في أي خطوة من هذا القرار.
بالنسبة إلى التواريخ داخل العقد، تعتمد الإجابة على الساعة التي يطلبها الكود. تستخدم Luxon، وهي مكتبة التواريخ التي تقف وراء تعبيرات n8n، المنطقة الزمنية لـn8n؛ لذلك يتبع $now و$today ترتيب سير العمل ثم المثيل نفسه الذي يتبعه المشغّل. أما new Date() العادي في JavaScript داخل عقدة Code، فيطلب المنطقة الزمنية من نظام التشغيل؛ لذلك يتبع TZ. وهذا الانقسام هو مصدر معظم الالتباس هنا: قد يعمل المشغّل بصورة صحيحة، بينما تكون كل الطوابع الزمنية التي يكتبها سير العمل متأخرة أو متقدمة بعدة ساعات.
ضع القيم الثلاث كلها في ملف Compose
ضع TZ وGENERIC_TIMEZONE بجانب بعضهما في الملف، حتى لا يضبط أحدهما وينسى الآخر. يوضّح المقطع أدناه الجزء المتعلق بالمنطقة الزمنية من خدمة عاملة. أما بقية الملف، والـreverse proxy والشهادة، فتأتي من دليل n8n مستضاف ذاتياً على VPS خلف HTTPS.
services:
n8n:
image: docker.n8n.io/n8nio/n8n
restart: unless-stopped
ports:
- "127.0.0.1:5678:5678"
environment:
- GENERIC_TIMEZONE=Europe/Berlin
- TZ=Europe/Berlin
- N8N_RUNNERS_ENABLED=true
- N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
volumes:
- n8n_data:/home/node/.n8n
volumes:
n8n_data:طبّقه باستخدام docker compose up -d، وليس باستخدام docker compose restart. تؤدي إعادة التشغيل إلى تشغيل الحاوية نفسها مجدداً بالبيئة التي أُنشئت بها، لذلك تتغير الملفات ولا تتغير العملية قيد التشغيل. يكتشف up -d تغيّر البيئة ويعيد إنشاء الحاوية. إذا احتفظت بهذه القيم في ملف env بدلاً من تعريفها داخل الملف، تنطبق قاعدة إعادة الإنشاء نفسها، ويشرح دليل ملف env والأسرار في Compose مصدر قراءة ذلك الملف.
استخدم اسم منطقة زمنية من IANA (Internet Assigned Numbers Authority) بصيغة Region/City، مثل Europe/Berlin أو America/Sao_Paulo. تتضمن هذه الأسماء قواعد التوقيت الصيفي لذلك المكان، لذلك يتغير الإزاحة عندما تتغير الساعات المحلية. أما اسم الإزاحة الثابتة مثل Etc/GMT+5 فلا يتغير مع الفصول، كما أن إشارته معكوسة مقارنة بما قد تتوقعه. شغّل LC_ALL=C TZ=Etc/GMT+5 date +%z، وسيطبع -0500. تجنّب هذه الأسماء.
لماذا يؤدي ضبط واحد منهما فقط إلى إصلاح المشكلة جزئياً
اضبط GENERIC_TIMEZONE وحده، وسيعمل Schedule Trigger في الساعة التي حددتها، بينما يبقى كل ما يقرأ من نظام التشغيل على UTC. ستعيد عقدة Code التي تستدعي new Date().toString() سلسلة زمنية بتوقيت UTC، وستحمل أسطر سجلات الحاوية طابعاً زمنياً بتوقيت UTC، كما سينتقل أي اسم ملف يُنشأ من ساعة النظام عند منتصف الليل الخطأ.
اضبط TZ وحده، ويحدث العكس. ستطبع docker compose exec n8n date الوقت المحلي، ما يبدو نجاحاً، بينما يظل Schedule Trigger مضبوطاً على America/New_York ويعمل قبل الساعة التي طلبتها أو بعدها بست ساعات. هذا هو الإعداد الذي يستهلك أكبر قدر من الوقت، لأن الفحص الذي يجريه معظم الأشخاص أولاً هو الفحص الذي ينجح الآن.
اضبط المنطقة الزمنية لسير العمل، ثم غيّر GENERIC_TIMEZONE لاحقاً، وسيتجاهل سير العمل ذلك التغيير. تكون قيمة سير العمل هي السائدة، وتظل كذلك إلى أن يفتح أحدهم إعدادات ذلك سير العمل. إذا كان سير عمل واحد يعمل في ساعة غير معتادة بينما تعمل بقية الأعمال المجاورة بصورة صحيحة، فغالباً ما يكون السبب هو ذلك.
تحقّق من الساعات بدلاً من التخمين
قارن وقت المضيف ووقت الحاوية مباشرةً.
date
docker compose exec n8n date
docker compose exec n8n printenv TZ GENERIC_TIMEZONEيجب أن يطبع الأمران الأولان وقت الساعة الفعلي نفسه بعد ضبط TZ. يطبع printenv سطراً واحداً لكل متغير موجود، ولذلك يعني ظهور سطرين أن المتغيرين مضبوطان، بينما يعني ظهور سطر واحد أنك ترى حالة الإصلاح الجزئي.
اسأل الآن n8n نفسه من داخل سير عمل، لأن shell الحاوية لا يمكنه إخبارك بالمنطقة الزمنية على مستوى سير العمل. أضف عقدة Code إلى سير العمل الذي لا يعمل كما ينبغي، ثم شغّلها مرة واحدة باستخدام Execute Workflow.
return [
{
json: {
n8n_time: $now.toISO(),
n8n_zone: $now.zoneName,
system_time: new Date().toString(),
},
},
];تمثل n8n_zone المنطقة الزمنية التي سيستخدمها Schedule Trigger في سير العمل، بعد حلّها وفق ترتيب سير العمل ثم المثيل، ولذلك فهي تجيب عن السؤال مباشرةً. وتحمل system_time المنطقة الزمنية الخاصة بالحاوية كما يحددها TZ. شغّل ذلك في سير العمل الذي لا يعمل كما ينبغي، وليس في سير عمل جديد، لأن إعداد المنطقة الزمنية على مستوى سير العمل ينتقل مع سير العمل. إذا اختلفت القيمتان، فقد وجدت المشكلة من دون فتح ملف إعداد واحد.
تعبيرات Cron في عقدة Schedule Trigger
توفّر Schedule Trigger فواصل زمنية ثابتة تبدأ من الثواني وتصل إلى الأشهر، إضافةً إلى خيار Custom (Cron) لأي فواصل لا تغطيها هذه الخيارات. تُقرأ تعبيرات Cron وفق المنطقة الزمنية الفعلية لسير العمل، لذا تعني 0 6 * * * الساعة 06:00 في تلك المنطقة، وليس الساعة 06:00 بالتوقيت العالمي UTC. يمكن لصق تعبير من خمسة حقول من crontab guru كما هو. يقبل n8n أيضاً حقلاً اختيارياً للثواني، ويضعه جدول الحقول في وثائق البرنامج أولاً: الثانية، الدقيقة، الساعة، يوم الشهر، الشهر، يوم الأسبوع.
لا تُشفِّر الإزاحة يدوياً. إن كتابة 0 4 * * * على مثيل يستخدم UTC للوصول إلى الساعة 06:00 في Berlin تكون صحيحة في الشتاء، لكنها تتقدم ساعة واحدة طوال الصيف، لأن Berlin تعمل بالتوقيت UTC+1 في الشتاء وUTC+2 في الصيف. حدّد المنطقة الزمنية، واكتب الساعة المحلية التي تقصدها فعلياً.
ما الذي يفعله التوقيت الصيفي بمهمة مجدولة عند 02:30
لا يضمن التوقيت المحلي على الساعة حدوثاً زمنياً محدداً. يختفي ساعة واحدة مرتين في السنة، وتتكرر ساعة واحدة، وتتأثر أي مهمة مجدولة خلال هاتين الفترتين. يمكنك مراقبة ذلك باستخدام date على أي خادم Linux، من دون استخدام n8n.
LC_ALL=C TZ=Europe/Berlin date -d '2027-03-28 02:30'date: invalid date '2027-03-28 02:30'هذا ليس خطأ مطبعياً في الأمر. في 2027-03-28 تنتقل ساعة برلين مباشرة من 02:00 إلى 03:00، لذلك لا وجود للتوقيت المحلي 02:30 في ذلك اليوم، ويرفض date تحويله إلى حدوث زمني محدد. لا توجد لحظة يمكن أن تعمل فيها مهمة مثبتة على التوقيت المحلي 02:30. أما التوقيتان المجاوران فصحيحان: يحل date -d '2027-03-28 01:30' إلى CET، ويحل date -d '2027-03-28 03:30' إلى CEST.
يكون الانتقال الخريفي معكوساً. في 2027-10-31 تعود ساعة برلين من 03:00 إلى 02:00، لذلك يحدث التوقيت 02:30 مرتين.
LC_ALL=C TZ=Europe/Berlin date -d '2027-10-31 02:30 CEST' '+%s'
LC_ALL=C TZ=Europe/Berlin date -d '2027-10-31 02:30 CET' '+%s'1824942600
1824946200حدوثان زمنيان مختلفان، وكلاهما يسمى 02:30 بالتوقيت المحلي، ويفصل بينهما 3600 ثانية. قد تعمل المهمة المثبتة على ذلك التوقيت مرتين، أو تعمل مرة واحدة عند ساعة لم يحددها أحد. لا تناسب أي من النتيجتين تشغيل الفوترة أو تدوير النسخ الاحتياطية. انقل الجدول إلى خارج هذه الفترة. في معظم المناطق الزمنية الأوروبية وأمريكا الشمالية، تمتد الفترة الخطرة من 00:00 إلى 03:00 بالتوقيت المحلي.
جدولة البنية التحتية وفق UTC وعرض الوقت المحلي للأشخاص
تفصل القاعدة العملية بين وظيفتين للمنطقة الزمنية. تحتاج الآلات إلى فترة زمنية ثابتة. ويحتاج الأشخاص إلى ساعة سهلة القراءة.
- بالنسبة إلى المهام التي لا يراقبها أحد، اضبط المنطقة الزمنية لسير العمل على UTC. تشمل هذه الفئة النسخ الاحتياطية، وتهيئة ذاكرة التخزين المؤقت، وشحن السجلات، وإنشاء التقارير. في UTC، يكون الفاصل بين تشغيلين هو تماماً الفاصل الذي كتبته في كل يوم من أيام السنة، لأن UTC لا تطبق التوقيت الصيفي.
- بالنسبة إلى المهام التي يقرأ نتائجها شخص، أبقِ الجدول وفق UTC وحوّل الوقت عند عرضه. ينجز تعبير واحد ذلك:
{{ $now.setZone('Europe/Berlin').toFormat('yyyy-MM-dd HH:mm') }}يضع الوقت المحلي في نص الرسالة، بينما يبقى المشغّل ثابتاً.
ينطبق الفصل نفسه خارج n8n. عندما يعمل جزء من الأتمتة كـخدمة ومؤقت systemd على VPS، يُقرأ سطر OnCalendar فيه وفق المنطقة الزمنية للنظام، وهي ساعة رابعة لها إعدادها الخاص. إن أبقيت كل أدوات الجدولة على UTC، فلن تحتاج إلا إلى تذكّر قاعدة واحدة بدلاً من أربع قواعد. وهذا مهم أيضاً لأي شيء يلخّص فترة زمنية، لأن سير عمل وكيل AI في n8n يطلب أرقام الأمس سيستخدم بهدوء مدة مختلفة مقدارها 24 ساعة، وفق المنطقة التي حُدِّدت بها الفترة.
أوضاع الفشل والمخرجات التي ستراها
كل شيء يحدث بفارق نحو ست ساعات. لم يتم تعيين GENERIC_TIMEZONE، لذلك يُطبَّق الإعداد الافتراضي المضمّن America/New_York. لا يطبع docker compose exec n8n printenv GENERIC_TIMEZONE أي شيء على الإطلاق. عيّن القيمة، ثم أنشئ الحاوية من جديد.
عدّلت ملف Compose ولم يتغير شيء. شغّلت docker compose restart، لذلك احتفظت الحاوية ببيئتها الأصلية. شغّل docker compose up -d، ثم تحقّق باستخدام docker compose exec n8n printenv TZ.
المشغّل صحيح والطوابع الزمنية خاطئة. تم تعيين GENERIC_TIMEZONE فقط. ما يزال new Date() في عقدة Code يقرأ التوقيت UTC من نظام التشغيل. عيّن TZ إلى القيمة نفسها، ثم أنشئ الحاوية من جديد.
يتجاهل أحد مسارات العمل إعداد المثيل. يحتوي مسار العمل هذا على منطقته الزمنية الخاصة في إعداداته، وتتغلب هذه القيمة على GENERIC_TIMEZONE. افتح اللوحة، ثم قائمة النقاط الثلاث، ثم Settings، ثم Timezone.
نُفِّذت مهمة يومية مرتين، أو تخطّت يوماً واحداً خلال هذه السنة. يقع وقتها المجدول ضمن فترة انتقال التوقيت الصيفي. غيّر الساعة، أو انقل مسار العمل هذا إلى UTC.
FAQ
لماذا يُشغَّل مشغّل الجدولة في n8n في ساعة غير صحيحة؟
تستخدم العملية الزمنية منطقة زمنية مختلفة عما تفترضه. يختار n8n المنطقة الزمنية للعملية إذا كانت محددة، وإلا يستخدم المنطقة الزمنية للمثيل من GENERIC_TIMEZONE، وإلا يستخدم الإعداد الافتراضي المضمّن وهو America/New_York. إذا لم يحدد أحد GENERIC_TIMEZONE في مثيل مستضاف ذاتياً، فستُجدول المهام وفق توقيت New York، لا UTC. لذلك نادراً ما يتطابق الفارق مع فارقك أنت عن UTC. نفّذ docker compose exec n8n printenv GENERIC_TIMEZONE. عدم ظهور أي ناتج يعني أنه لم يُضبط مطلقاً.
ما الفرق بين TZ و GENERIC_TIMEZONE في n8n؟
TZ هي المنطقة الزمنية لنظام التشغيل داخل الحاوية. وتتحكم في القيمة التي يعرضها date داخل الحاوية، والطوابع الزمنية التي تظهر في أسطر سجل الحاوية، والقيمة التي يعرضها new Date() في عقدة Code، وما تراه أيٌّ من البرامج النصية التي تشغّلها داخلها. أما GENERIC_TIMEZONE فهي المنطقة الزمنية لمثيل n8n، وتستخدمها عقد الجدولة وتعبيرات Luxon مثل $now. يؤدي ضبط أحدهما دون الآخر إلى تشغيل صحيح مع طوابع زمنية خاطئة، أو طوابع زمنية صحيحة مع تشغيل يبعد عدة ساعات عن الوقت المطلوب. اضبطهما على القيمة نفسها.
هل أضبط المنطقة الزمنية للعملية أم GENERIC_TIMEZONE؟
اضبط GENERIC_TIMEZONE كإعداد افتراضي للمثيل بأكمله، واستخدم إعداد كل عملية فقط عندما تنتمي عملية معينة فعلاً إلى منطقة زمنية أخرى. تتغلب قيمة العملية على قيمة المثيل، ولا تتبع التغييرات اللاحقة في GENERIC_TIMEZONE. لذلك يصعب اكتشاف تجاوز قديم لإعداد العملية بعد مرور أشهر.
ماذا يحدث لمهمة مجدولة عند 02:30 عندما تتغير الساعات؟
قد يختفي هذا الوقت المحلي أو يحدث مرتين. يعرض LC_ALL=C TZ=Europe/Berlin date -d '2027-03-28 02:30' القيمة date: invalid date '2027-03-28 02:30'، لأن ساعة Berlin تقفز في ذلك اليوم من 02:00 إلى 03:00. في 2027-10-31، تشير قراءة الساعة المحلية نفسها إلى لحظتين تفصل بينهما ساعة واحدة. أبقِ المهام المجدولة خارج النطاق المحلي من 00:00 إلى 03:00، أو اضبط العملية على UTC وحوّل الوقت إلى التوقيت المحلي فقط عند عرضه على شخص.