الأدلة التقنية

أتمتة سير العمل: متى تكتب كوداً ومتى تستخدم أداة؟

ما هي أتمتة سير العمل، ومتى تكفيك أداة مثل n8n ومتى تحتاج كوداً أو محركاً آخر، وما الذي تقرره قبل أن تبني أي أتمتة.

أتمتة سير العمل: متى تكتب كوداً ومتى تستخدم أداة؟

في أغلب الشركات توجد مهام تتكرر كل يوم بالطريقة نفسها: طلب جديد يصل من المتجر فينسخه موظف إلى نظام الفواتير ثم يرسل رسالة لفريق الشحن، أو موظف جديد ينضم فيفتح أحدهم تذكرة لكل نظام يحتاج حساباً، أو نموذج يملؤه عميل فتنقل بياناته يدوياً إلى جدول. وفي يوم ما يقرر أحد المطورين أن يكتب سكربتاً صغيراً على أحد السيرفرات يقوم بهذا العمل، أو يربط فريق العمليات الأنظمة بأداة مثل Zapier أو n8n في ساعة واحدة، وتعمل الأتمتة بشكل جميل لعدة أشهر، إلى أن يتوقف نظام الفواتير لدقائق فتضيع طلبات دون أن يلاحظ أحد، أو يعيد السكربت المحاولة فيصدر للعميل فاتورتين، أو يغادر الشخص الذي بنى الأتمتة ولا يعرف أحد أين هي ولا كيف تعمل.

وهذا المقال هو الأول في سلسلة أتمتة سير العمل Workflow Automation على عرب رووت، ونبدأ فيه بالمفاهيم قبل الأدوات، لأن المشاكل التي ذكرناها لا تحلها أداة بعينها، وإنما تحلها قرارات تتخذها قبل أن تبني أي أتمتة.

المواضيع في السلسلة:

وسوف نناقش في هذا المقال ما يلي:

  • ما هي أتمتة سير العمل Workflow، وأجزاؤها الثلاثة: المشغل Trigger والخطوات Steps والأفعال Actions.
  • خمس طرق لربط الأنظمة: كود التكامل، وأدوات الأتمتة المرئية iPaaS، والروبوتات RPA، وإدارة العمليات BPM، ومحركات التنسيق Orchestration، ومتى تناسب كل واحدة.
  • ما يجب أن تقرره قبل بناء أي أتمتة: منع التكرار Idempotency، وإعادة المحاولة Retry، وسير عمل الأخطاء Error Workflow، وحدود الطلبات Rate Limits، والأسرار، وأمان ال Webhooks، والمراقبة، ومن يملك الأتمتة.
  • متى لا تؤتمت أصلاً.
  • مختبر: التدفق نفسه مكتوباً كسكربت ومبنياً في n8n، ثم نحقن فيه أخطاء ونقارن ما يحدث.

ما هي أتمتة سير العمل Workflow؟

أتمتة سير العمل Workflow Automation هي أن تنفذ سلسلة من الخطوات بين عدة أنظمة دون أن يقوم بها إنسان في كل مرة، فيبدأ التشغيل حدث ما، ثم تمر البيانات على خطوات تعالجها، ثم تنتهي بأفعال تغير شيئاً في نظام آخر. وهي تشبه الوصفة المكتوبة في المطبخ: الطلب يصل، فتأخذ المكونات وتجهزها بترتيب معروف، ثم يخرج الطبق، والفرق أن الوصفة هنا ينفذها برنامج، وأنه لا يتذوق الطبق قبل أن يرسله، فإذا كانت المكونات خاطئة خرج الطبق خاطئاً بثقة كاملة.

والصورة التالية تبين الأجزاء الثلاثة لأي أتمتة:

ثلاثة صناديق من اليمين إلى اليسار: المشغل Trigger وفيه Webhook وSchedule وPolling وForm، ثم الخطوات Steps وفيها Transform وIf/Switch وLoop/Merge، ثم الأفعال Actions وفيها API وEmail/Chat وDatabase، وتحتها شريط Execution فيه ثلاثة تشغيلات أحدها فشل في الخطوة الثانية
المشغل Trigger يبدأ التشغيل، والخطوات تعالج البيانات، والأفعال تغير شيئاً في نظام آخر، وكل تشغيل يسجل وحده

وفي الصورة أعلاه لاحظ التالي:

  • المشغل Trigger: الحدث الذي يبدأ التشغيل، وأشهر أنواعه ال Webhook (نظام آخر يرسل طلب HTTP عند حدوث شيء)، والجدولة Schedule (كل ساعة أو كل يوم)، والفحص الدوري Polling (سؤال نظام آخر كل دقيقة: هل يوجد شيء جديد؟)، والنموذج Form أو المحادثة التي يرسلها إنسان.
  • الخطوات Steps: ما تفعله بالبيانات بين البداية والنهاية، مثل تحويل الحقول Transform، والتفرع حسب قيمة If/Switch، والتكرار على قائمة عناصر Loop، ودمج نتائج من مصدرين Merge. وتسمى كل خطوة في أغلب الأدوات عقدة Node.
  • الأفعال Actions: الخطوات التي تغير شيئاً خارج الأتمتة، مثل إنشاء فاتورة عبر API، أو إرسال رسالة، أو إضافة سطر في قاعدة بيانات. وهذه هي الخطوات التي تحتاج أكبر قدر من الحذر، لأن تكرارها يعني فاتورة ثانية أو رسالة ثانية للعميل.
  • التشغيل Execution: كل مرة تعمل فيها الأتمتة تسمى تشغيلاً، والأداة الجيدة تحفظ لكل تشغيل مدخلاته ومخرجاته والخطأ إن وجد، وهذا السجل هو أول ما تحتاجه عندما يسألك أحدهم: لماذا لم تصل فاتورة الطلب رقم كذا؟

وسوف نستخدم في السلسلة كلها مصطلح سير العمل Workflow للأتمتة الكاملة من المشغل Trigger إلى آخر فعل، ومصطلح بيانات الاعتماد Credentials للمفاتيح وكلمات المرور التي تستخدمها الأتمتة للدخول إلى الأنظمة الأخرى.

خمس طرق لربط الأنظمة

عندما تريد أن تربط نظامين أمامك خمس طرق مختلفة، وكثير من مشاكل الأتمتة تأتي من اختيار الطريقة الخطأ للعمل، مثل بناء عملية مالية طويلة فيها موافقات في أداة أتمتة مرئية، أو كتابة خدمة كاملة لعمل تكفيه ثلاث خطوات في n8n. والصورة التالية تلخص الطرق الخمس:

خمسة أعمدة: كود التكامل Integration Code يبنيه المطورون ويناسب المنطق الذي هو جزء من المنتج، وأدوات الأتمتة المرئية iPaaS مثل n8n وZapier وMake تناسب ربط تطبيقات لها API بخطوات قصيرة، وRPA مثل Power Automate Desktop يناسب النظام القديم الذي لا يملك API، وBPM/BPMN مثل Camunda وFlowable يناسب العمليات الطويلة التي فيها موافقات ومهام بشرية، ومحركات التنسيق Orchestration مثل Temporal وAirflow تناسب العمل الطويل الذي يجب أن يكمل بعد أي توقف
كل طريقة تحل مشكلة مختلفة، والخطأ الشائع أن تستخدم الطريقة التي تعرفها لكل شيء

والجدول التالي يقارن بينها:

الطريقةما هينقاط القوةنقاط الضعف
كود التكامل Integration Codeسكربت أو خدمة يكتبها المطورون وتعيش مع بقية الكودتحكم كامل، واختبارات، ومراجعة كود Code Review، ونسخ Versioning في Gitكل ما تقدمه الأدوات الأخرى جاهزاً (سجل التشغيلات Executions، وإعادة المحاولة Retry، وبيانات الاعتماد، والتنبيه) عليك كتابته بنفسك
أدوات الأتمتة المرئية iPaaSمنصة فيها مئات الموصلات Connectors جاهزة، تبني فيها سير العمل Workflow بسحب العقد Nodes وربطهاسريعة البناء، وسجل لكل تشغيل، وإعادة محاولة وتنبيه من الإعدادات، ويستطيع غير المبرمج قراءتهاالمنطق المعقد يصبح شبكة عقد صعبة القراءة، والاختبار والمراجعة أضعف من الكود، والتسعير في الخدمات السحابية يرتفع مع عدد التشغيلات Executions
الروبوتات RPAبرنامج يحاكي الإنسان: يفتح الواجهة ويضغط الأزرار ويقرأ الشاشةالطريقة الوحيدة أحياناً مع نظام قديم لا يملك API ولا قاعدة بيانات يمكن الوصول إليهاهش جداً، فأي تغيير في شكل الواجهة أو بطء في تحميلها يكسر الروبوت
إدارة العمليات BPMمحرك ينفذ عملية مرسومة بمعيار BPMN، فيها مهام بشرية وموافقات ومؤقتاتالرسم هو نفسه ما ينفذ، فيقرؤه المحلل والمدير والمطور، ويناسب العمليات التي تستمر أياماً أو أسابيع مع تدقيق كامليحتاج مطورين ومحللين متخصصين، وهو أثقل بكثير مما تحتاجه أتمتة بسيطة
محركات التنسيق Orchestrationمحرك يحفظ حالة كل خطوة ويستكمل بعد أي توقف، ويكتب سير العمل Workflow فيه بالكودضمانات تشغيل قوية للعمل الطويل والحرج، مثل الطلبات المالية ومعالجة البيانات الكبيرةبنية إضافية تحتاج تشغيلاً ومراقبة، ونموذج برمجة مختلف يحتاج وقتاً للتعلم

وبعض التفاصيل التي تساعدك على التمييز بينها:

  • BPMN هو معيار رسم العمليات الذي تصدره مجموعة OMG، وقد اعتمد أيضاً كمعيار دولي باسم ISO/IEC 19510:2013، وأدوات مثل Camunda وFlowable تنفذ الرسم نفسه، فالمهمة البشرية في الرسم تظهر فعلاً في قائمة مهام الموظف المسؤول.
  • Temporal يقدم ما يسميه التنفيذ الدائم Durable Execution: تكتب سير العمل Workflow كدالة عادية في Go أو Java أو Python أو TypeScript، ويحفظ المحرك تاريخ كل خطوة، فإذا توقف السيرفر في منتصفها أكمل من حيث توقف. وقد ذكرناه في المعاملات الموزعة: Saga ونمط Outbox كأحد طرق تنفيذ ال Saga.
  • Apache Airflow مصمم لخطوط البيانات Data Pipelines المجدولة، حيث تكتب سير العمل Workflow كرسم من المهام Python DAG ويشغله المجدول Scheduler كل ساعة أو كل يوم، وهو لا يناسب الاستجابة لطلب واحد من عميل خلال ثانية.

وقد يتساءل البعض: إذا كان كود التكامل يعطيني تحكماً كاملاً، فلماذا لا أكتب كل شيء بالكود؟ والإجابة أن الكود يعطيك التحكم ولكنه لا يعطيك ما حوله: سجل لكل تشغيل يراه فريق الدعم دون أن يفتح السيرفر، وزر لإعادة تشغيل ما فشل، ومكان آمن لبيانات الاعتماد، وتنبيه عند الفشل، وموصلات جاهزة لعشرات الأنظمة. فإذا كانت الأتمتة جزءاً من منتجك أو منطقاً حرجاً يحتاج اختبارات فاكتبها بالكود، وإذا كانت ربطاً بين تطبيقات داخلية يتغير كل شهر فالأداة توفر عليك أسابيع من كتابة ما هو موجود فيها أصلاً.

والأسئلة التالية تساعدك على الاختيار:

  1. هل الأتمتة جزء من المنتج الذي تبيعه، أم ربط داخلي بين أنظمة الشركة؟ الأولى مكانها الكود غالباً، والثانية مكانها الأداة.
  2. هل تستمر العملية دقائق أم أياماً؟ العملية التي تنتظر موافقة مدير لمدة أسبوع تحتاج محرك BPM أو محرك تنسيق، وليس سير عمل Workflow ينتظر في الذاكرة.
  3. هل يملك النظام الآخر API؟ إذا لم يكن يملكها فاسأل أولاً عن قاعدة البيانات أو ملفات التصدير، واجعل RPA آخر خيار.
  4. من سيعدل الأتمتة بعد سنة؟ إذا كان فريق العمليات فالأداة المرئية أنسب، وإذا كان فريق التطوير فالكود أو محرك التنسيق.
  5. ما تكلفة الخطأ؟ أتمتة ترسل تذكيراً بموعد اجتماع ليست كأتمتة تصدر فواتير أو تحذف حسابات، والثانية تحتاج كل ما في القسم التالي.

وسوف نقارن الأدوات نفسها بتراخيصها وأسعارها في أدوات أتمتة سير العمل Workflow.

ما الذي تقرره قبل بناء أي أتمتة؟

الأتمتة التي تعمل في يوم التجربة سهلة، والصعب أن تبقى صحيحة عندما يتأخر نظام أو يرسل حدثاً مرتين أو يغير شكل بياناته. والقرارات التالية هي ما يفرق بين الاثنين، وكل واحد منها له قسم أدناه:

القرارالسؤال الذي يجيب عنه
منع التكرار Idempotencyماذا يحدث إذا وصل الحدث نفسه مرتين؟
إعادة المحاولة Retry والانتظار Backoffكم مرة نعيد المحاولة، وكم ننتظر بين كل محاولة؟
سير عمل الأخطاء Error Workflow وقائمة الفاشل Dead Letterمن يعرف بالفشل، وأين يذهب ما فشل نهائياً؟
حدود الطلبات Rate Limitsكم طلباً يقبل الطرف الآخر في الدقيقة؟
بيانات الاعتماد Credentialsأين تحفظ المفاتيح، ومن يستطيع رؤيتها؟
أمان ال Webhooksكيف تعرف أن الطلب جاء فعلاً من النظام الذي تتوقعه؟
المراقبة Observabilityكيف تعرف أن الأتمتة توقفت قبل أن يخبرك العميل؟
الملكية Ownershipمن يصلح الأتمتة عندما تتعطل، ومن يعرف أنها موجودة أصلاً؟

منع التكرار Idempotency

افترض دائماً أن كل حدث سوف يصلك أكثر من مرة، والسبب أن أغلب الأنظمة التي ترسل Webhooks تعيد الإرسال إذا لم يصلها رد ناجح في وقته، فمثلاً تذكر Stripe في توثيق ال Webhooks أنها تعيد إرسال الحدث الذي لم يصل لمدة تصل إلى ثلاثة أيام، وأن نقطة الاستقبال قد تستلم الحدث نفسه أكثر من مرة، وتنصح بحفظ معرفات الأحداث التي عالجتها. وأنت أيضاً سوف تعيد المحاولة عندما يتأخر رد النظام الآخر، وهنا يظهر أخطر سيناريو في الأتمتة: الطلب نجح فعلاً في الطرف الآخر، ولكن الرد تأخر فظننت أنه فشل وأعدت المحاولة.

والحل أن يكون لكل فعل مفتاح ثابت يعرف به، مثل رقم الطلب، فترسله مع كل محاولة في ترويسة Idempotency-Key إذا كان الطرف الآخر يدعمها، أو تحفظ في جدول عندك أنك عالجت هذا المفتاح قبل أن تنفذ الفعل. وقد شرحنا هذا بالتفصيل في Idempotency: كيف تجعل العملية آمنة عند تكرارها، وسوف نرى في المختبر أدناه كم فاتورة مكررة تصدر بدونه.

إعادة المحاولة Retry والانتظار Backoff

الأخطاء المؤقتة كثيرة: الخدمة تعيد تشغيل نفسها، أو الشبكة تتأخر، أو الطرف الآخر يرد بالرمز 503. لذلك يجب أن تعيد المحاولة، ولكن بثلاث قواعد:

  • أعد المحاولة على الأخطاء المؤقتة فقط: الرموز 429 و503 وانتهاء المهلة Timeout تستحق محاولة ثانية، أما 400 و401 و404 فلن تنجح مهما أعدتها، لأن الخطأ في الطلب نفسه.
  • انتظر وقتاً يزيد مع كل محاولة: ثانية ثم ثانيتين ثم أربع، وأضف قدراً عشوائياً صغيراً Jitter حتى لا تعود كل التشغيلات Executions الفاشلة في اللحظة نفسها.
  • ضع حداً لعدد المحاولات: ثلاث أو خمس محاولات ثم يذهب التشغيل إلى مسار الأخطاء، والسبب أن إعادة المحاولة Retry بلا حد تضاعف الحمل على خدمة متعبة أصلاً.

وقد شرحنا لماذا تسقط إعادة المحاولة Retry غير المضبوطة أنظمة كاملة في كيف تمنع خدمة بطيئة من إسقاط نظامك كله، ولاحظ أن إعادة المحاولة Retry وحدها بدون منع التكرار تحول كل مهلة منتهية إلى فعل مكرر.

سير عمل الأخطاء Error Workflow وقائمة الفاشل Dead Letter

بعد أن تنتهي المحاولات يجب أن يحدث شيئان: أن يعرف إنسان بالفشل، وأن يبقى ما فشل محفوظاً حتى يعاد تشغيله، والصورة التالية تبين المسارين اللذين يجب أن تخطط لهما:

في الأعلى أربع مراحل: خطوة فشلت برمز 503 أو انتهاء مهلة، ثم إعادة المحاولة بانتظار يزيد ثلاث مرات، ثم Error Workflow ينبه صاحب الأتمتة، ثم Dead Letter قائمة بما فشل نهائياً يراجعه إنسان. وفي الأسفل الأتمتة ترسل طلب إنشاء فاتورة، والفاتورة تنشأ لكن الرد يتأخر فتنتهي المهلة، وإعادة المحاولة بدون مفتاح تنشئ فاتورتين، والحل مفتاح Idempotency-Key ثابت
الخطأ المؤقت تعالجه إعادة المحاولة Retry ثم التنبيه، والنجاح الذي لم يصلك رده يعالجه مفتاح منع التكرار

وفي أدوات الأتمتة يسمى المسار الأول سير عمل الأخطاء Error Workflow، وهو سير عمل Workflow منفصل يبدأ عندما يفشل سير عمل Workflow آخر، فيرسل تنبيهاً فيه اسم سير العمل Workflow ورابط التشغيل الفاشل والخطأ، كما في عقدة Error Trigger في n8n. أما قائمة الفاشل Dead Letter فهي المكان الذي يبقى فيه ما فشل نهائياً، وفي أدوات الأتمتة هي غالباً قائمة التشغيلات Executions الفاشلة نفسها، وفي الطوابير هي طابور منفصل كما شرحنا في الطابور Queue للمبتدئين. والمهم أن تحدد من يراجع هذه القائمة ومتى، لأن قائمة لا يفتحها أحد هي طريقة أبطأ لفقد البيانات.

حدود الطلبات Rate Limits

أغلب الخدمات تحدد عدد الطلبات المسموح بها في الدقيقة أو الثانية، وعندما تتجاوزه ترد بالرمز 429 Too Many Requests، وقد ترسل معه ترويسة Retry-After تخبرك كم تنتظر، كما في RFC 6585. وهذه المشكلة لا تظهر في التجربة بعشرة سجلات، وتظهر في اليوم الذي تستورد فيه خمسة آلاف عميل دفعة واحدة، فتتوقف الأتمتة في منتصفها ولا تعرف أي السجلات وصل وأيها لم يصل. لذلك اقرأ حدود كل API قبل أن تبني عليها، وقسم العمل الكبير إلى دفعات Batches بينها انتظار، واحترم ترويسة Retry-After عندما تصلك.

بيانات الاعتماد Credentials

كل أتمتة تحمل مفاتيح لأنظمة أخرى، وغالباً بصلاحيات أوسع مما تحتاجه، لأن من بناها أخذ مفتاح المدير لأنه الأسرع. وهذا يجعل أداة الأتمتة من أثمن الأهداف في شبكتك، فمن يدخل إليها يدخل إلى كل ما تربطه. لذلك اتبع التالي:

  • أنشئ لكل أتمتة حساب خدمة Service Account بأقل صلاحية تحتاجها، وليس حساب موظف ولا حساب المدير.
  • لا تضع المفاتيح داخل الكود أو داخل حقول سير العمل Workflow كنص، واستخدم مخزن بيانات الاعتماد في الأداة، أو مخزن أسرار خارجي، وقد شرحنا كيف تكتشف المفاتيح المسربة في اكتشاف الأسرار المسربة.
  • اعرف أين يحفظ مفتاح التشفير الذي تشفر به الأداة بيانات الاعتماد، واحفظ نسخة منه، فسوف نرى في تثبيت n8n أن فقده يعني فقد كل بيانات الاعتماد حتى لو كانت قاعدة البيانات سليمة.

أمان ال Webhooks

عنوان ال Webhook هو Endpoint علني يستطيع أي شخص أن يرسل إليه طلباً، فإذا كانت الأتمتة تصدر فاتورة أو تنشئ حساباً لكل طلب يصلها فالمخترق يستطيع أن يفعل ذلك أيضاً. والحل أن تتحقق من توقيع الطلب Signature الذي يرسله النظام المرسل، وهو غالباً HMAC على جسم الطلب بمفتاح مشترك، مع وقت الإرسال حتى لا يعاد إرسال طلب قديم. وقد شرحنا التحقق بالتفصيل في قسم ال Webhooks في أمن واجهات API، وشرحنا HMAC والمقارنة في زمن ثابت في التشفير للمطورين. ولاحظ أن أدوات الأتمتة نفسها قد تخطئ في هذا، فمثلاً نشرت n8n في سبتمبر 2026 تحذيراً برقم CVE-2026-86080 عن مشغل GitHub Trigger الذي كان يقبل الطلبات دون أن يتحقق من توقيعها في حالة معينة، وأصلحته في الإصدارات 1.123.76 و2.37.7 و2.38.2.

المراقبة Observability

الأتمتة التي تتوقف لا تصدر صوتاً، فالسكربت الذي توقف عن العمل قبل أسبوعين يبدو تماماً مثل السكربت الذي لم يجد شيئاً يعمل عليه. لذلك راقب ثلاثة أشياء:

  • نسبة التشغيلات Executions الفاشلة: مع تنبيه عندما تتجاوز حداً معيناً.
  • غياب التشغيلات Executions: إذا كانت الأتمتة تعمل عادة مئة مرة في اليوم ثم لم تعمل منذ ثلاث ساعات فهذا عطل، حتى لو لم يظهر أي خطأ، لأن المشغل Trigger نفسه قد يكون هو الذي توقف.
  • مدة التشغيل: لأن الأتمتة التي كانت تنتهي في ثانية وأصبحت تأخذ دقيقة تخبرك أن نظاماً ما يتعب قبل أن يسقط.

واحفظ سجل التشغيلات Executions مدة كافية لتجيب عن سؤال «لماذا لم تصل فاتورة الطلب الذي وصل قبل أسبوعين؟»، وانتبه في الوقت نفسه إلى أن هذا السجل يحفظ البيانات التي مرت بالأتمتة، وفيها غالباً بيانات شخصية، فحدد مدة الحفظ وصلاحية الاطلاع عليه.

من يملك الأتمتة؟

هذا هو السؤال الذي ينساه الجميع، والسبب أن أدوات الأتمتة تجعل البناء سهلاً جداً، فيبني كل قسم أتمتته على حسابه، وبعد سنتين تجد مئات سير العمل Workflow لا يعرف أحد أيها مهم، وبعضها يعمل بحساب موظف غادر الشركة، فإذا عطل حسابه توقفت الأتمتة دون أن يفهم أحد السبب. لذلك ضع لكل أتمتة تدخل الإنتاج سجلاً بسيطاً فيه الحقول التالية:

الحقلمثال
الاسم وما تفعله في جملةإنشاء فاتورة لكل طلب جديد في المتجر وإبلاغ فريق الشحن
المالك ونائبهفريق المالية، والنائب من فريق التقنية
الأنظمة وحسابات الخدمةالمتجر ونظام الفواتير والمحادثة، بحساب خدمة لكل منها
ما يحدث عند الفشلثلاث محاولات ثم تنبيه في قناة المالية، والفاشل يراجع يومياً
تأثير التوقفالطلبات تبقى دون فاتورة، والعميل لا يستلم إيصاله
آخر مراجعةأكتوبر 2026

وهذا السجل هو نفسه فكرة سجل الخدمات في إدارة خدمات تقنية المعلومات ITSM، فالأتمتة التي تعتمد عليها المالية هي خدمة يجب أن يعرف فريق الدعم من يتصل به عندما تتعطل.

متى لا تؤتمت؟

ليست كل مهمة متكررة تستحق الأتمتة، ولا تقم بأتمتة المهمة في الحالات التالية:

  • العملية نفسها غير واضحة: إذا كان كل موظف ينفذ المهمة بطريقة مختلفة فالأتمتة سوف تنفذ طريقة واحدة منها بسرعة أكبر، فاتفق على العملية أولاً.
  • المهمة نادرة والخطأ فيها مكلف: مهمة تحدث مرتين في السنة ولها أثر مالي كبير تحتاج قائمة تحقق Checklist وإنساناً منتبهاً، وليس أتمتة لا يتذكر أحد كيف تعمل عندما يأتي موعدها.
  • الوقت الذي توفره أقل من وقت صيانتها: احسب الدقائق التي توفرها في الشهر، ثم احسب الوقت الذي سوف تقضيه في إصلاحها كلما تغير أحد الأنظمة، فالأتمتة بين نظامين يتغيران كل شهر قد تكلف أكثر مما توفر.
  • القرار يحتاج حكماً بشرياً: قبول طلب استثناء أو رفض شكوى عميل، وهنا أتمت جمع البيانات وتجهيزها، واترك القرار لإنسان بخطوة موافقة صريحة.
  • لا يوجد مالك: إذا لم تجد من يقبل أن يكون مسؤولاً عنها عندما تتعطل، فهي ليست جاهزة للإنتاج.

مختبر: التدفق نفسه كسكربت وفي n8n، مع أخطاء مقصودة

الآن سوف نبني أتمتة صغيرة بطريقتين ونكسرها بالطريقة نفسها: طلب يصل عبر Webhook، فننشئ له فاتورة في API، ثم نرسل إشعاراً. والطرف الآخر خادم وهمي Mock كتبناه بمكتبة Python القياسية، يستطيع أن يفشل بطريقتين:

  • flaky: كل طلب ثالث يرد بالرمز 503 قبل أن ينشئ الفاتورة، وهذا هو الخطأ المؤقت.
  • slow: الطلب الأول لكل رقم طلب ينشئ الفاتورة ثم يتأخر في الرد ثانيتين ونصف، والمتصل ينتظر ثانية واحدة فقط، وهذا هو «النجاح الذي لا يصلك رده».

والخادم الوهمي يحترم ترويسة Idempotency-Key، ويعد في النهاية الفواتير والطلبات المختلفة والفواتير المكررة. وكل شيء يعمل في Containers على شبكة Docker واحدة، بإصدارات python:3.13.16-slim وn8nio/n8n:2.43.4.

ملفات المختبر

الخادم الوهمي mock.py:

"""Fake invoice API + notification sink for the automation lab (stdlib only).
POST /invoices      creates an invoice; honours the Idempotency-Key header
POST /notify        records a notification
POST /mode?m=X      ok | flaky (every 3rd request fails with 503) | slow (first call per order is slow)
GET  /stats         counters as JSON; POST /reset clears them
"""
import json, os, threading, time
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs

lock = threading.Lock()
S = {}
def reset():
    S.update(mode=os.environ.get("MODE", "ok"), requests=0, injected=0,
             invoices=[], by_key={}, seen_orders=set(), notifications=[])
reset()

class H(BaseHTTPRequestHandler):
    def log_message(self, *a): pass
    def send(self, code, obj):
        b = json.dumps(obj).encode()
        self.send_response(code); self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(b))); self.end_headers(); self.wfile.write(b)
    def body(self):
        n = int(self.headers.get("Content-Length") or 0)
        return json.loads(self.rfile.read(n) or b"{}")
    def do_GET(self):
        if self.path.startswith("/stats"):
            with lock:
                orders = [i["order_id"] for i in S["invoices"]]
                self.send(200, {"mode": S["mode"], "requests": S["requests"], "injected_failures": S["injected"],
                                "invoices": len(orders), "unique_orders": len(set(orders)),
                                "duplicate_invoices": len(orders) - len(set(orders)),
                                "notifications": len(S["notifications"])})
        else: self.send(404, {})
    def do_POST(self):
        u = urlparse(self.path)
        if u.path == "/reset":
            with lock: reset()
            return self.send(200, {"ok": True})
        if u.path == "/mode":
            with lock: S["mode"] = parse_qs(u.query)["m"][0]
            return self.send(200, {"mode": S["mode"]})
        if u.path == "/notify":
            b = self.body()
            with lock: S["notifications"].append(b)
            return self.send(200, {"ok": True})
        if u.path == "/invoices":
            b = self.body(); key = self.headers.get("Idempotency-Key"); slow = False
            with lock:
                S["requests"] += 1
                if S["mode"] == "flaky" and S["requests"] % 3 == 0:
                    S["injected"] += 1
                    return self.send(503, {"error": "service unavailable (injected)"})
                if key and key in S["by_key"]:
                    return self.send(200, S["by_key"][key])
                inv = {"invoice_id": f"INV-{len(S['invoices']) + 1:04d}", "order_id": b.get("order_id")}
                S["invoices"].append(inv)
                if key: S["by_key"][key] = inv
                if S["mode"] == "slow" and b.get("order_id") not in S["seen_orders"]:
                    slow = True
                S["seen_orders"].add(b.get("order_id"))
            if slow:
                S["injected"] += 1
                time.sleep(2.5)   # invoice already created, but the caller times out after 1 s
            return self.send(201, inv)
        self.send(404, {})

ThreadingHTTPServer(("0.0.0.0", 9000), H).serve_forever()

والأتمتة نفسها كسكربت flow.py، مع متغيرين يحددان هل يعيد المحاولة وهل يرسل مفتاح منع التكرار:

"""The same automation written as a small script: order webhook -> create invoice -> notify.
RETRIES=0 means no retry; IDEMPOTENCY=1 sends the order id as Idempotency-Key."""
import json, os, time, urllib.request
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler

API = os.environ.get("API", "http://lab-auto-mock:9000")
RETRIES = int(os.environ.get("RETRIES", "0"))
IDEM = os.environ.get("IDEMPOTENCY", "0") == "1"

def post(path, data, headers=None):
    req = urllib.request.Request(API + path, json.dumps(data).encode(),
                                 {"Content-Type": "application/json", **(headers or {})}, method="POST")
    with urllib.request.urlopen(req, timeout=1) as r:
        return json.loads(r.read())

def create_invoice(order):
    headers = {"Idempotency-Key": order["order_id"]} if IDEM else {}
    for attempt in range(RETRIES + 1):
        try:
            return post("/invoices", {"order_id": order["order_id"], "amount": order["amount"]}, headers)
        except Exception as e:
            if attempt == RETRIES:
                raise
            time.sleep(0.2 * 2 ** attempt)   # backoff: 0.2 s, 0.4 s, 0.8 s

class H(BaseHTTPRequestHandler):
    def log_message(self, *a): pass
    def do_POST(self):
        order = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
        try:
            inv = create_invoice(order)
            post("/notify", {"text": f"order {order['order_id']} -> {inv['invoice_id']}"})
            code, out = 200, inv
        except Exception as e:
            print(f"order {order['order_id']} failed: {e!r}", flush=True)
            code, out = 500, {"error": str(e)}
        b = json.dumps(out).encode()
        self.send_response(code); self.send_header("Content-Length", str(len(b))); self.end_headers(); self.wfile.write(b)

ThreadingHTTPServer(("0.0.0.0", 8000), H).serve_forever()

والمرسل send.py يرسل 30 طلباً واحداً بعد الآخر ويعد الردود:

"""Sends N orders one after another to a URL and prints how many got 2xx."""
import json, sys, urllib.request, urllib.error, collections
url, n, prefix = sys.argv[1], int(sys.argv[2]), sys.argv[3]
codes = collections.Counter()
for i in range(1, n + 1):
    body = json.dumps({"order_id": f"{prefix}-{i:03d}", "amount": 100 + i}).encode()
    req = urllib.request.Request(url, body, {"Content-Type": "application/json"}, method="POST")
    try:
        with urllib.request.urlopen(req, timeout=30) as r: codes[r.status] += 1
    except urllib.error.HTTPError as e: codes[e.code] += 1
    except Exception as e: codes[type(e).__name__] += 1
print(f"sent={n} responses={dict(sorted(codes.items(), key=str))}")

والملف run-script.sh يصفر عدادات الخادم الوهمي ويضبط طريقة الفشل ثم يرسل الطلبات ويطبع العدادات:

# usage: sh run-script.sh <mode> <target-url> <prefix>
M=http://lab-auto-mock:9000
python - "$M" "$1" <<'PY'
import sys, urllib.request
m, mode = sys.argv[1], sys.argv[2]
for p in ("/reset", "/mode?m=" + mode):
    urllib.request.urlopen(urllib.request.Request(m + p, b"", method="POST"))
PY
python send.py "$2" 30 "$3"
python -c "import urllib.request;print(urllib.request.urlopen('$M/stats').read().decode())"

التشغيل

نشغل الخادم الوهمي وثلاث نسخ من السكربت: الأولى دون إعادة محاولة، والثانية تعيد ثلاث مرات، والثالثة تعيد ثلاث مرات وترسل المفتاح:

docker network create lab-auto-net
docker run -d --name lab-auto-mock --network lab-auto-net -v "$PWD:/lab:ro" -w /lab python:3.13.16-slim python -u mock.py
docker run -d --name lab-auto-script0 --network lab-auto-net -e RETRIES=0 -e IDEMPOTENCY=0 -v "$PWD:/lab:ro" -w /lab python:3.13.16-slim python -u flow.py
docker run -d --name lab-auto-script1 --network lab-auto-net -e RETRIES=3 -e IDEMPOTENCY=0 -v "$PWD:/lab:ro" -w /lab python:3.13.16-slim python -u flow.py
docker run -d --name lab-auto-script2 --network lab-auto-net -e RETRIES=3 -e IDEMPOTENCY=1 -v "$PWD:/lab:ro" -w /lab python:3.13.16-slim python -u flow.py
docker run -d --name lab-auto-client --network lab-auto-net -v "$PWD:/lab:ro" -w /lab python:3.13.16-slim sleep infinity
docker exec lab-auto-client sh run-script.sh flaky http://lab-auto-script0:8000/ S0

والمخرج للسكربت في الحالات الأربع سوف يكون كما يلي (أضفنا سطر العنوان قبل كل حالة):

== script S0 mode=flaky
sent=30 responses={200: 20, 500: 10}
{"mode": "flaky", "requests": 30, "injected_failures": 10, "invoices": 20, "unique_orders": 20, "duplicate_invoices": 0, "notifications": 20}
== script S1 mode=flaky
sent=30 responses={200: 30}
{"mode": "flaky", "requests": 44, "injected_failures": 14, "invoices": 30, "unique_orders": 30, "duplicate_invoices": 0, "notifications": 30}
== script S1s mode=slow
sent=30 responses={200: 29, 500: 1}
{"mode": "slow", "requests": 62, "injected_failures": 30, "invoices": 62, "unique_orders": 30, "duplicate_invoices": 32, "notifications": 29}
== script S2s mode=slow
sent=30 responses={200: 30}
{"mode": "slow", "requests": 60, "injected_failures": 30, "invoices": 30, "unique_orders": 30, "duplicate_invoices": 0, "notifications": 30}

وفي المخرج أعلاه لاحظ التالي:

  • دون إعادة محاولة: عشرة طلبات من ثلاثين فشلت، والسكربت كتب سطراً في سجله لكل منها ثم نسيها، فلا يوجد مكان تعيد منه تشغيلها إلا أن يعيد العميل إرسالها.
  • مع إعادة المحاولة Retry: نجحت الثلاثون كلها في حالة الخطأ المؤقت، بأربعة وأربعين طلباً بدل ثلاثين.
  • مع إعادة المحاولة Retry في حالة الرد المتأخر: 62 فاتورة لثلاثين طلباً، أي 32 فاتورة مكررة. والأسوأ أن الطلب S1s-003 استنفد كل محاولاته لأن المحاولات التالية أيضاً تجاوزت المهلة على جهازنا المشغول، فأنشئت له أربع فواتير ورد السكربت للعميل بالرمز 500، أي أن العميل دفع أربع مرات وقيل له إن الطلب فشل.
  • مع إعادة المحاولة Retry والمفتاح: ثلاثون فاتورة لثلاثين طلباً دون أي تكرار، رغم أن عدد الطلبات إلى الخادم ستون.

التدفق نفسه في n8n

بنينا في n8n ثلاثة سير عمل Workflow بالعقد Nodes نفسها: Webhook ثم HTTP Request لإنشاء الفاتورة ثم HTTP Request للإشعار، والفرق بينها في إعدادات عقدة الفاتورة فقط: الأول دون إعادة محاولة، والثاني مع Retry On Fail بثلاث محاولات بينها ثانية، والثالث يضيف ترويسة Idempotency-Key من رقم الطلب. والعقدة في الثالث كما يلي:

{
  "parameters": {
    "method": "POST",
    "url": "http://lab-auto-mock:9000/invoices",
    "sendHeaders": true,
    "headerParameters": {
      "parameters": [
        {
          "name": "Idempotency-Key",
          "value": "={{ $json.body.order_id }}"
        }
      ]
    },
    "sendBody": true,
    "specifyBody": "json",
    "jsonBody": "={{ JSON.stringify({ order_id: $json.body.order_id, amount: $json.body.amount }) }}",
    "options": {
      "timeout": 1000
    }
  },
  "id": "b0000000-0000-4000-8000-000000000003",
  "name": "Create invoice",
  "type": "n8n-nodes-base.httpRequest",
  "typeVersion": 4.2,
  "position": [
    260,
    0
  ],
  "retryOnFail": true,
  "maxTries": 3,
  "waitBetweenTries": 1000
}

ثم شغلنا n8n في Container واحد بقاعدة SQLite الافتراضية، فهذا يكفي للتجربة، وأنشأنا حساب المالك، واستوردنا الملفات ونشرناها بسطر الأوامر، ثم أعدنا تشغيله حتى تعمل ال Webhooks:

docker run -d --name lab-auto-n8n-a --network lab-auto-net -v lab-auto-n8n-a-data:/home/node/.n8n -v "$PWD/wf:/wf:ro" n8nio/n8n:2.43.4
docker exec lab-auto-n8n-a n8n import:workflow --input=/wf/order-a.json
docker exec lab-auto-n8n-a n8n publish:workflow --id=labAutoARetry001
docker restart lab-auto-n8n-a
docker exec lab-auto-client sh run-script.sh slow http://lab-auto-n8n-a:5678/webhook/order-a NAs

والمخرج للحالات الأربع سوف يكون كما يلي:

== n8n order-0 mode=flaky
sent=30 responses={200: 20, 500: 10}
{"mode": "flaky", "requests": 30, "injected_failures": 10, "invoices": 20, "unique_orders": 20, "duplicate_invoices": 0, "notifications": 20}
== n8n order-a mode=flaky
sent=30 responses={200: 30}
{"mode": "flaky", "requests": 44, "injected_failures": 14, "invoices": 30, "unique_orders": 30, "duplicate_invoices": 0, "notifications": 30}
== n8n order-a mode=slow
sent=30 responses={200: 30}
{"mode": "slow", "requests": 60, "injected_failures": 30, "invoices": 60, "unique_orders": 30, "duplicate_invoices": 30, "notifications": 30}
== n8n order-b mode=slow
sent=30 responses={200: 30}
{"mode": "slow", "requests": 60, "injected_failures": 30, "invoices": 30, "unique_orders": 30, "duplicate_invoices": 0, "notifications": 30}

ثم قرأنا جدول التشغيلات Executions من قاعدة بيانات n8n لنرى ماذا حفظت الأداة عن كل تشغيل:

('Order to invoice (no retry)', 'error', 10)
('Order to invoice (no retry)', 'success', 20)
('Order to invoice (retry + idempotency key)', 'success', 30)
('Order to invoice (retry)', 'success', 60)

وفي المخرج أعلاه لاحظ التالي:

  • الأرقام في الحالات الأربع هي نفسها تقريباً في السكربت وفي n8n، والسبب أن الأداة لا تغير منطق الأتمتة، فإعادة المحاولة Retry دون مفتاح أنتجت 30 فاتورة مكررة في n8n كما في السكربت.
  • الفرق في ما يبقى بعد الفشل: التشغيلات Executions العشرة الفاشلة محفوظة في n8n بحالة error، ومعها بيانات الطلب، فالتشغيل الفاشل الأول مثلاً يحمل رقم الطلب N0-003 والرد 503، ويستطيع من يراجعها أن يعيد تشغيلها من الواجهة، أما السكربت فلم يترك إلا سطراً في سجل ال Container.
  • عقدة n8n تنتظر وقتاً ثابتاً بين المحاولات (ثانية في إعدادنا)، بينما كتبنا في السكربت انتظاراً يتضاعف، فإذا احتجت انتظاراً يتضاعف في n8n فعليك بناؤه بعقد إضافية.

والخلاصة من المختبر أن الأداة أعطتنا سجلاً وإعادة تشغيل دون كتابة سطر، ولكنها لم تحمنا من الفاتورة المكررة، فهذا قرار تصميم عليك أنت أن تتخذه في أي أداة.

حذف المختبر

docker rm -f lab-auto-n8n-a lab-auto-client lab-auto-script0 lab-auto-script1 lab-auto-script2 lab-auto-mock
docker volume rm lab-auto-n8n-a-data
docker network rm lab-auto-net

أخطاء شائعة

  • اختبار المسار السعيد فقط: الأتمتة تجرب بطلب واحد صحيح ثم تدخل الإنتاج، ولا يعرف أحد ماذا تفعل عندما يرد نظام بالرمز 503 أو يتأخر ثلاث ثوان.
  • إعادة المحاولة Retry دون مفتاح منع التكرار: تحول كل مهلة منتهية إلى فعل مكرر، كما رأينا في المختبر.
  • بيانات اعتماد بحساب شخصي: تتوقف الأتمتة يوم يغادر صاحب الحساب أو يغير كلمة مروره.
  • Webhook دون تحقق من التوقيع: أي شخص يعرف العنوان يستطيع تشغيل الأتمتة ببيانات من اختياره.
  • لا أحد يراجع التشغيلات Executions الفاشلة: القائمة تكبر بصمت حتى يسأل العميل عن فاتورته.
  • منطق المنتج داخل أداة الأتمتة: قواعد التسعير أو الصلاحيات تعيش في عقد لا تختبر ولا تراجع، ثم يصبح تغييرها مخاطرة.

تدريب عملي

  1. اكتب قائمة بكل أتمتة تعمل في شركتك اليوم، ولكل منها املأ جدول الملكية الموجود أعلاه، وستجد غالباً واحدة على الأقل لا يعرف أحد من يملكها.
  2. اختر أتمتة واحدة مهمة، واسأل: ماذا يحدث إذا وصل الحدث نفسه مرتين؟ وماذا يحدث إذا لم يرد النظام الآخر لمدة عشر دقائق؟ ومن يعرف بذلك؟
  3. شغل المختبر وغير في الخادم الوهمي نسبة الفشل إلى كل طلب ثان بدل كل طلب ثالث، ولاحظ كم طلباً يبقى فاشلاً بعد ثلاث محاولات.

الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

  • كل أتمتة لها مشغل Trigger وخطوات Steps وأفعال Actions، والأفعال هي التي تحتاج أكبر قدر من الحذر.
  • كود التكامل يناسب منطق المنتج، وأدوات الأتمتة المرئية تناسب الربط الداخلي بين التطبيقات، وBPM للعمليات الطويلة التي فيها بشر، ومحركات التنسيق للعمل الحرج الذي يجب أن يكمل بعد أي توقف، وRPA آخر خيار.
  • افترض أن كل حدث يصل أكثر من مرة، واجعل لكل فعل مفتاح منع تكرار.
  • أعد المحاولة على الأخطاء المؤقتة فقط، بانتظار يزيد وحد أقصى، ثم سير عمل Workflow أخطاء ينبه إنساناً.
  • تحقق من توقيع كل Webhook، واستخدم حسابات خدمة بأقل صلاحية.
  • راقب غياب التشغيلات Executions وليس فشلها فقط، وضع لكل أتمتة مالكاً ونائباً.

وفي المقال القادم بإذن الله سوف نقارن أدوات أتمتة سير العمل Workflow بتراخيصها وأسعارها، ونبني التدفق نفسه في ثلاث منها ونقيس الفرق.

سجل التحديثات

  • أكتوبر 2026: كتابة المقال، وتشغيل المختبر على n8n 2.43.4 وPython 3.13.16، والاطلاع على المصادر المذكورة بتاريخ 14 أكتوبر 2026.
Could not load content
نشرة عرب رووت | ArabRoot

معرفة تستحق مكاناً في بريدك.

مقالات مختارة وأدوات مفيدة وأفكار لمشروعك القادم، في رسالة واحدة كل أسبوع.

يمكنك إلغاء الاشتراك متى شئت. الخصوصية

تم استلام طلبك. افتح بريدك واضغط رابط التأكيد لإتمام الاشتراك.