يا جماعة الخير، السلام عليكم ورحمة الله وبركاته، معكم أخوكم أبو عمر.
قبل فترة، كنت شغال على مشروع لأحد العملاء، وكان طالب نظام بسيط لأتمتة تسجيل الدخول في منصته. قال لي: “يا أبو عمر، بدي إشي بسيط، المستخدم يبعت إيميله، يوصله كود، يدخله، وخلصنا”. قلت في نفسي “أبسط منها ما فيه”. وبدأت أبني الـ workflow في n8n على الطريقة التقليدية: Webhook يستقبل الإيميل، يتصل بقاعدة البيانات، يرسل الكود، وينتظر التحقق. كله في مسار واحد خطي.
بعد يومين، رجع العميل وقال: “ممتاز، بس بدنا نضيف حالة لو المستخدم حسابه معلّق”. بسيطة، ضفت عقدة IF. بعدها بيوم: “ولو طلب كود جديد والكود القديم لسا ما انتهى؟”. كمان IF. “ولو الكود انتهت صلاحيته؟”. كمان IF. “ولو…”. بعد أسبوع، تحوّل الـ workflow اللي كان “بسيط” إلى وحش من عقد الـ IF المتشابكة، صرت أسميه “عجقة السباغيتي”. كل تعديل صغير كان يأخذ ساعات من الخوف والقلق ليكون في إشي انضرب في مكان ثاني. وقتها صفنت وقلت لحالي: “يا عمي، الأكيد في طريقة أحسن من هيك!”.
وهون بدأت رحلتي مع مفهوم الـ “Agent-based Workflow”. القصة وما فيها، بدل ما الـ workflow يكون مجرد سلسلة أوامر غبية، خليته يصير “وكيل” (Agent) ذكي: عنده ذاكرة، بقدر يقرر، وبفهم السياق. واليوم، بدي أشارككم هاي الطريقة اللي غيّرت نظرتي لـ n8n من أداة أتمتة بسيطة إلى منصة تطوير متكاملة.
لماذا Agent Workflow وليس Workflow تقليدي؟
خلينا نكون صريحين، معظمنا لما يبدأ في n8n، بيفكر بالطريقة الخطية:
Trigger → Query DB → IF → Email → Done
هذا النموذج ممتاز للمهام البسيطة، لكنه ينهار تمامًا عندما تتعقد الأمور. متى؟
- تعدد الحالات: عندما يكون لديك حالات مختلفة مثل (مستخدم موجود / محظور / لم يؤكد بريده / الكود منتهي الصلاحية).
- الحاجة لذاكرة مؤقتة (State): عندما تحتاج لتذكر خطوة سابقة، مثل “هذا المستخدم طلب كود قبل دقيقة”.
- منطق قرار ديناميكي: عندما لا يكون القرار مجرد “نعم” أو “لا”، بل يعتمد على عدة عوامل متغيرة.
- مسارات متعددة: عندما تريد توجيه المستخدمين إلى مسارات مختلفة تمامًا بناءً على حالتهم (مثلاً، مستخدم جديد يرى صفحة ترحيب، ومستخدم قديم يرى لوحة التحكم).
هنا يأتي دور الـ Agent-based Workflow. الفكرة هي أن الـ Workflow نفسه يتصرف كـ “وكيل” ذكي:
- يقرر (Decides): يحلل البيانات ويختار الخطوة التالية.
- يتحقق (Verifies): يتأكد من صحة المعلومات وصلاحيات المستخدم.
- يتذكر (Remembers): يحتفظ بحالة مؤقتة (State) لاتخاذ قرارات مستقبلية.
- يوجه (Routes): يفتح مسارات مختلفة حسب السياق.
باختصار، أنت لا تبني مجرد أتمتة، أنت تبني “عقلًا” صغيرًا داخل n8n.
السيناريو الكامل (Use Case Definition)
لنجعل الأمر عمليًا، سنبني نظامًا يقوم بالمهام التالية:
- استقبال طلب تحقق: مستخدم يرسل بريده الإلكتروني ليبدأ عملية تسجيل الدخول.
- التحقق من المستخدم في MySQL: هل المستخدم موجود ونشط في قاعدة بياناتنا؟
- توليد OTP آمن ومؤقت: إنشاء رمز تحقق (One-Time Password) صالح لبضع دقائق فقط.
- إرسال OTP إلى البريد الإلكتروني: إيصال الرمز للمستخدم.
- انتظار التحقق: النظام ينتظر من المستخدم إدخال الرمز الذي وصله.
- فتح مسار خاص: بعد التحقق الناجح، يتم توجيه المستخدم إلى workflow آخر مخصص للمستخدمين الموثوقين فقط.
كل هذا مع ضمان أعلى معايير الأمان، عزل الحالات (State Isolation)، وقابلية التوسع مستقبلًا.
المعمارية العامة للنظام (System Architecture)
قبل كتابة أي كود، يجب أن نرسم الخريطة. المعمارية التي سنتبعها تبدو هكذا:
User Request (Email)
↓
Trigger (Webhook)
↓
Agent Controller (The Brain 🧠)
↓
MySQL Verification (Is user valid?)
↓
OTP Generator + Store (Create & Save secure OTP)
↓
Email Sender (Send OTP to user)
↓
--- Waiting Phase ---
↓
OTP Validation (User submits OTP)
↓
Authorized Path (Unlock private workflow)
نصيحة من أبو عمر: الـ “Agent” هنا ليس عقدة (Node) واحدة في n8n، بل هو نمط تفكير وطريقة لتصميم الـ workflow بأكمله. هو العقل المدبر الذي يربط كل هذه الخطوات معًا.
أولاً: تصميم قاعدة البيانات (MySQL)
الأساس المتين يبدأ من قاعدة بيانات نظيفة ومنظمة. سنحتاج إلى جدولين:
جدول المستخدمين (users)
هذا الجدول يخزن معلومات المستخدمين الأساسية.
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
is_active BOOLEAN DEFAULT true,
is_verified BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
جدول رموز التحقق (user_otp)
هذا الجدول يخزن رموز OTP المؤقتة المرتبطة بكل مستخدم.
CREATE TABLE user_otp (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
otp_hash VARCHAR(255) NOT NULL,
expires_at DATETIME NOT NULL,
used BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
⚠️ ملاحظة أمنية هامة: لاحظ أننا نخزن
otp_hashوليس الـ OTP نفسه. لا تخزّن OTP كنص صريح أبداً! هذه من أكبر الأخطاء الأمنية التي يمكن أن ترتكبها. سنتحدث عن كيفية عمل الـ Hashing لاحقًا.
ثانياً: نقطة الدخول (Agent Entry Point)
كل نظام يحتاج إلى باب أمامي. في حالتنا، سيكون Webhook Trigger في n8n. هذا الـ Webhook سيستقبل طلبًا بسيطًا بصيغة JSON يحتوي على بريد المستخدم.
Input:
{
"email": "user@example.com"
}
هذا هو المدخل الوحيد لعملية طلب التحقق. البساطة هنا قوة.
ثالثاً: العقل المركزي (Agent Controller)
هنا يبدأ السحر. بدلًا من التفرع مباشرة، نستخدم عقدة Code Node (أو Function Node قديمًا) لتعمل كـ “متحكم” أو “عقل” للـ Agent. وظيفتها بسيطة لكنها محورية: قراءة المدخلات وتحديد الخطوة الأولى في العملية.
في هذه العقدة، نكتب كود JavaScript بسيطًا:
// Agent Controller
// The first thought of our Agent
// We receive the email from the webhook
const email = $json.email;
// We set the initial state or "step" for the agent to perform
return [{
json: {
email: email,
step: 'VERIFY_USER' // The next action is to verify the user
}
}];
المتغير step هو ما يجعل هذا الـ workflow شبيهًا بالـ Agent. إنه يمثل “الحالة الذهنية” الحالية للـ Agent، ويخبرنا بما يجب فعله بعد ذلك. الآن، لم يعد الـ workflow مجرد سلسلة، بل أصبح لديه “نية” (intention).
رابعاً: التحقق من المستخدم في MySQL
بناءً على الحالة VERIFY_USER، نستخدم عقدة MySQL Node لتنفيذ استعلام للتحقق من وجود المستخدم ونشاطه.
Query:
SELECT id, email, is_active FROM users WHERE email = '{{ $json.email }}' AND is_active = true;
هنا، يواجه الـ Agent أول قرار حقيقي له. نستخدم عقدة IF Node لفصل المسارات بناءً على نتيجة الاستعلام:
- الحالة 1 (❌ مستخدم غير موجود): إذا لم يُرجع الاستعلام أي نتائج، فهذا يعني أن البريد الإلكتروني غير مسجل. يمكننا هنا إنهاء الـ workflow أو إرسال رسالة “المستخدم غير موجود”.
- الحالة 2 (❌ مستخدم معطّل): إذا كان المستخدم موجودًا ولكن
is_active = false(وهو ما لن يحدث مع الاستعلام أعلاه، لكنه جيد للتفكير فيه)، يمكننا إرسال رسالة “حسابك معطل”. - الحالة 3 (✅ مستخدم صالح): إذا وجدنا المستخدم وهو نشط، ننتقل إلى الخطوة التالية: توليد OTP.
خامساً: توليد OTP (بشكل آمن)
الآن، وبعد أن تأكدنا من هوية المستخدم، نحتاج إلى إنشاء رمز سري له. نستخدم عقدة Code Node مرة أخرى لهذا الغرض.
const crypto = require('crypto');
// 1. Generate a simple 6-digit random number
const otp = Math.floor(100000 + Math.random() * 900000).toString();
// 2. Create a secure hash of the OTP. We will store this hash, not the OTP itself.
const hash = crypto.createHash('sha256').update(otp).digest('hex');
// 3. Set an expiration time (e.g., 5 minutes from now)
const expires_at = new Date(Date.now() + 5 * 60 * 1000).toISOString().slice(0, 19).replace('T', ' ');
return [{
json: {
...$input.item.json, // Keep previous data like user_id and email
otp: otp, // The plain OTP to be sent to the user
otp_hash: hash, // The hash to be stored in the database
expires_at: expires_at
}
}];
نصيحة من أبو عمر: صلاحية الـ OTP يجب أن تكون قصيرة جدًا (3-5 دقائق). هذا يقلل من نافذة الفرصة لأي هجوم محتمل. لا تجعلها صالحة لساعات!
سادساً: تخزين OTP وربطه بالمستخدم
قبل إرسال الرمز للمستخدم، يجب أن نخزّن الـ Hash الخاص به في قاعدة البيانات. نستخدم عقدة MySQL Node مرة أخرى لتنفيذ أمر INSERT.
INSERT INTO user_otp (user_id, otp_hash, expires_at)
VALUES ({{ $json.id }}, '{{ $json.otp_hash }}', '{{ $json.expires_at }}');
هذه الخطوة تضمن أن لدينا سجلاً آمناً للرمز الذي تم إنشاؤه وتاريخ انتهاء صلاحيته.
سابعاً: إرسال OTP عبر البريد الإلكتروني
هذه هي الخطوة التي يتفاعل فيها نظامنا مع المستخدم. باستخدام أي عقدة لإرسال البريد (Send Email, SMTP, Mailgun, etc)، نرسل رسالة بسيطة ومباشرة.
محتوى الرسالة:
مرحبًا،
رمز التحقق الخاص بك هو: {{ $json.otp }}
هذا الرمز صالح لمدة 5 دقائق فقط.
نصيحة أمنية: لا تضع أي روابط أو معلومات حساسة أخرى في هذا البريد. اجعله بسيطًا قدر الإمكان لتقليل مخاطر التصيد (Phishing).
ثامناً: الـ Agent في وضع الانتظار (OTP Validation Phase)
هنا يكمن الفرق الجوهري. الـ workflow الأول (دعنا نسميه Auth-Request-Agent) قد انتهت مهمته. لقد استقبل الطلب، تحقق من المستخدم، وأرسل الرمز. الآن، هو ينتظر.
الخطوة التالية تبدأ من خلال Trigger جديد، وهو Webhook آخر يستقبل الرمز من المستخدم. هذا يعني أننا نحتاج إلى workflow ثانٍ أو نقطة دخول ثانية (وهو ما أفضله).
Webhook Input للتحقق:
{
"email": "user@example.com",
"otp": "123456"
}
هذا الفصل بين “طلب الرمز” و “التحقق من الرمز” هو جوهر عزل المهام ويجعل النظام أكثر قوة وتنظيمًا.
تاسعاً: التحقق من صحة OTP
في الـ workflow الثاني (دعنا نسميه Auth-Verify-Agent)، نقوم بالخطوات التالية:
- جلب المستخدم والـ OTP من قاعدة البيانات:
ننفذ استعلامًا معقدًا بعض الشيء للتأكد من كل الشروط مرة واحدة.SELECT u.id as user_id, uo.otp_hash, uo.id as otp_id FROM user_otp uo JOIN users u ON uo.user_id = u.id WHERE u.email = '{{ $json.email }}' AND uo.used = false AND uo.expires_at > NOW() ORDER BY uo.created_at DESC LIMIT 1;هذا الاستعلام الذكي يجلب آخر OTP غير مستخدم وصالح للمستخدم المحدد.
- مقارنة الـ Hash:
الآن، في عقدة Code Node، نقوم بعملية الـ Hashing على الـ OTP الذي أدخله المستخدم ونقارنه بالـ Hash المخزن في قاعدة البيانات.const crypto = require('crypto'); const incomingOtp = $json.otp; const storedHash = $input.item.json.otp_hash; // From the MySQL query result // Hash the OTP provided by the user using the same algorithm const incomingHash = crypto.createHash('sha256').update(incomingOtp).digest('hex'); // Compare the hashes if (incomingHash !== storedHash) { // If they don't match, throw an error to stop the workflow throw new Error('Invalid or expired OTP.'); } // If they match, we proceed return [{ json: { ...$input.item.json, verified: true } }]; - تحديث حالة الـ OTP:
خطوة أمنية حرجة! بعد التحقق الناجح، يجب أن نحدّث سجل الـ OTP في قاعدة البيانات لجعله “مستخدَمًا” (used = true). هذا يمنع هجمات إعادة الاستخدام (Replay Attacks).UPDATE user_otp SET used = true WHERE id = {{ $json.otp_id }};
عاشراً: فتح المسار المخصص (Authorized Path)
هون المربط الفرس. بعد أن تم التحقق من المستخدم بنجاح، لا تكمل المنطق في نفس الـ workflow. هذا خطأ شائع يخلط بين منطق المصادقة (Authentication) ومنطق العمل (Business Logic).
الخيار الأفضل والأكثر أمانًا وقابلية للتوسع هو فصلهما تمامًا:
- Workflow A: هو الـ Agent الذي قمنا ببنائه للتو (Auth-Request + Auth-Verify).
- Workflow B: هو workflow خاص يحتوي على المنطق الذي لا يجب أن يصل إليه إلا المستخدمون الموثوقون (مثلاً: عرض بيانات حساسة، تحديث ملف شخصي، إلخ).
كيف نربط بينهما؟ باستخدام عقدة Execute Workflow أو استدعاء Webhook داخلي لـ Workflow B، مع تمرير بيانات المستخدم الذي تم التحقق منه.
في نهاية workflow التحقق، نضيف عقدة Execute Workflow ونمرر لها بيانات مثل:
{
"user_id": "{{ $json.user_id }}",
"role": "verified_user",
"authenticated_at": "{{ new Date().toISOString() }}"
}
بهذه الطريقة، Workflow B يثق تمامًا أن أي طلب يصله من Workflow A هو طلب موثوق ومصادق عليه.
لماذا هذا التصميم يعتبر “Agent” حقيقي؟
ما بنيناه ليس مجرد تسلسل خطوات، بل هو نظام يتمتع بخصائص الـ Agent:
- ✅ الوعي بالحالة (State Awareness): النظام “يعرف” ما إذا كان ينتظر OTP، أو إذا كان الـ OTP قد انتهت صلاحيته.
- ✅ الأمان (Secure OTP): استخدام الـ Hashing وتاريخ الصلاحية وتحديد الاستخدام مرة واحدة.
- ✅ التوجيه القائم على الدور (Role-based Routing): يوجه المستخدمين الموثوقين فقط إلى المسار المخصص.
- ✅ عزل الـ Workflows (Workflow Isolation): فصل منطق المصادقة عن منطق التطبيق.
- ✅ جاهز للإنتاج (Production Ready): هذه البنية قوية ويمكن الاعتماد عليها في بيئات العمل الحقيقية.
أخطاء قاتلة يجب تجنبها
خلال رحلتي، وقعت في الكثير من الأخطاء. تعلموا منها لتجنبها:
- ❌ تخزين OTP كنص واضح: أكبر خطأ أمني. استخدم الـ Hashing دائمًا.
- ❌ دمج المصادقة مع منطق العمل: يجعل الصيانة والتطوير كابوسًا. افصلهما دائمًا.
- ❌ عدم تحديد مدة صلاحية (TTL): يترك الـ OTP صالحًا إلى الأبد، مما يفتح ثغرة أمنية.
- ❌ استخدام workflow واحد لكل شيء: يؤدي إلى “عجقة السباغيتي” التي تحدثت عنها. قسّم عملك إلى workflows صغيرة ومتخصصة.
خاتمة: أنت لم تبنِ Workflow، بل بنيت نظام مصادقة 🚀
إذا وصلت إلى هنا، فما قمت ببنائه ليس مجرد “أتمتة” بسيطة. هذا نظام مصادقة مصغّر (Authentication Microservice) مبني بالكامل فوق منصة n8n.
لقد حولت n8n من مجرد أداة لتنفيذ المهام إلى منصة يمكنك بناء تطبيقات وأنظمة معقدة فوقها. ومن هذه النقطة، يمكنك التوسع بسهولة لإضافة ميزات متقدمة مثل:
- إصدار توكنات JWT (JSON Web Tokens) بعد التحقق الناجح.
- ربط النظام مع صلاحيات وأدوار (RBAC – Role-Based Access Control).
- بناء نظام SaaS (Software as a Service) كامل يعتمد على هذا الـ Agent للمصادقة.
الفارق كله يكمن في طريقة التفكير، وليس في عدد العقد (Nodes) التي تستخدمها. فكّر كمهندس أنظمة، وليس كمنفذ مهام. والله ولي التوفيق.