من الصفر: كيف تبني Trigger مخصص في n8n وتتجاوز حدود الأتمتة الجاهزة

قصة “حسّاس الحرارة” الذي علّمني درساً في الأتمتة

كنت أشتغل على مشروع أتمتة لمصنع أدوية قديم شوي. عندهم نظام مراقبة حرارة لغرف التخزين، نظام “عتيق” لكن شغال زي الساعة. المشكلة كانت إنه هالنظام ما بيبعث بياناته لا عن طريق HTTP Webhook ولا عنده API حديثة. كل اللي بيعمله هو إنه بيكتب قراءة الحرارة الجديدة في ملف نصي (log file) على سيرفر داخلي كل دقيقة. إذا الحرارة تجاوزت حد معين، بيكتب كلمة “ALARM” جنب القراءة.

المدير طلب مني أعمل نظام تنبيهات فوري يبعث إيميل ورسالة Slack لفريق الصيانة أول ما كلمة “ALARM” تظهر. فتحت n8n بكل ثقة، وبدأت أدور على Trigger مناسب. Webhook؟ ما بينفع. Cron Job كل دقيقة يقرأ الملف؟ ممكن، بس حسيته حل “ترقيعي” ومش فعال. شو القصة؟ كيف بدي أخلي n8n “يسمع” للتغييرات اللي بتصير على ملف في نظام ثاني؟

هنا أدركت الحقيقة: أنا كنت أعمل داخل صندوق صممه غيري. كل الـ Triggers الجاهزة بتفترض سيناريوهات شائعة، لكن العالم الحقيقي مليان حالات خاصة. الحل ما كان في البحث عن Node جديد، الحل كان إني أبني الـ Trigger الخاص فيي بنفسي. trigger “يستمع” لتغييرات الملف (File System Listener) ويفجّر الـ workflow بس عند الحاجة. ومن يومها، تغيرت نظرتي لـ n8n من مجرد “أداة” إلى “منصة تطوير” حقيقية.

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

لماذا تحتاج إلى Trigger مخصص أصلاً؟

خلونا نكون صريحين، معظم مستخدمي n8n عايشين حياتهم مبسوطين على الـ Triggers الجاهزة: Webhook لاستقبال الطلبات، Cron لتشغيل مجدول، و App Triggers للتكامل مع تطبيقات زي جوجل شيتس أو سلاك. كلها ممتازة ورائعة، لكنها بتشترك في مشكلة جوهرية واحدة:

أنت تعمل داخل حدود صمّمها غيرك.

متى تصطدم بهذه الحدود؟ عندما تحتاج لشيء أكثر تعقيداً:

  • Trigger يتعامل مع بروتوكول خاص: مثل MQTT لإنترنت الأشياء (IoT)، أو بروتوكول TCP/IP قديم، أو حتى الاستماع لتغييرات في قاعدة بيانات بشكل مباشر (Database Tailing).
  • Listener طويل العمر (Long-Running): مثل الاتصال بـ WebSocket API أو أي مصدر بيانات متدفق (Streaming) يبقى مفتوحاً لفترات طويلة.
  • Event داخلي من نظامك: مثل مراقبة تغييرات نظام الملفات (File System) كما في قصتي، أو الاستماع لأحداث من Message Queue مثل RabbitMQ أو Kafka.
  • Trigger له منطق معقد: تخيل أنك لا تريد تشغيل الـ Workflow إلا إذا حدثت 3 أحداث معينة خلال 5 دقائق. هذا المنطق المعقد لا يمكن تنفيذه بـ Trigger عادي.

هنا، لا يكفي “Node جاهز”… بل تحتاج Trigger حقيقي مخصص. هذه المقالة هي دليلك التقني والعملي لبناء هذا الـ Trigger، مع فهم معماري لما يحدث خلف الكواليس.

أولاً: كيف يفكّر n8n في الـ Trigger؟ (النموذج الذهني)

قبل ما نكتب سطر كود واحد، لازم نفهم كيف n8n “بيرى” العالم. هاي النقطة أهم من الكود نفسه.

الـ Trigger في n8n ليس:

  • مجرد زر “تشغيل”.
  • ولا هو مجرد Webhook URL.

الـ Trigger هو:

مصدر أحداث (Event Source) يقوم بإيقاظ الـ Workflow عند تحقق شرط معين.

معمارياً، التدفق يسير كالتالي:


External System / Timer / Stream
            ↓
        Trigger Node
            ↓
      Workflow Execution

والـ Trigger Node يختلف عن أي Node آخر (مثل node تعديل النصوص أو إرسال إيميل) في نقطتين حاسمتين:

  1. يعمل بدون Input: هو نقطة البداية، لا يوجد شيء قبله، لذا مصفوفة inputs في تعريفه تكون فارغة.
  2. له دورة حياة (Lifecycle) تُدار بالكامل: عندما تضغط “Activate” على الـ Workflow، يقوم n8n بتشغيل دالة خاصة في الـ Trigger ليبدأ “الاستماع”. وعندما تضغط “Deactivate”، يقوم n8n باستدعاء دالة أخرى لإيقافه وتنظيف أي عمليات معلقة.

نصيحة من أبو عمر

فكّر في الـ Trigger كـ “حارس” يقف على بوابة الـ Workflow. مهمته ليست تنفيذ العمل، بل تحديد متى يجب فتح البوابة والسماح للبيانات بالدخول لبدء العمل. إذا فهمت هذا المبدأ، نصف الطريق انقطع.

ثانياً: أنواع الـ Triggers من منظور هندسي

داخلياً، n8n يدعم عدة أنماط لبناء الـ Triggers. اختيارك للنمط يعتمد على طبيعة مصدر الأحداث لديك:

1. Webhook-Based Trigger

  • آلية العمل: يعتمد على استقبال البيانات (Event Push). النظام الخارجي هو من يبادر بإرسال البيانات إلى n8n.
  • الحالة (State): غالباً لا يحتاج لتخزين حالة (Stateless)، فكل طلب هو حدث منفصل.
  • مثال: استقبال بيانات من Stripe عند إتمام عملية دفع.
  • التقييم: سهل جداً في التنفيذ ولكنه محدود بالأنظمة التي تدعم Webhooks.

2. Polling Trigger

  • آلية العمل: يعتمد على فحص دوري (Timer + Fetch). الـ Trigger يقوم بسؤال النظام الخارجي كل فترة زمنية: “هل هناك شيء جديد؟”.
  • الحالة (State): يحتاج لإدارة الحالة (State Management) ليعرف ما هي آخر بيانات جلبها ويتجنب التكرار.
  • مثال: فحص بريد إلكتروني جديد كل 5 دقائق، أو جلب تغريدات جديدة من تويتر.
  • التقييم: شائع جداً في التكاملات (Integrations) وهو ما سنبنيه اليوم.

3. Long-Running Listener

  • آلية العمل: يفتح اتصالاً دائماً مع المصدر ويستمع للأحداث بشكل فوري.
  • الحالة (State): قد يحتاج لإدارة حالة أو لا، حسب البروتوكول.
  • مثال: الاتصال بسيرفر WebSocket، أو الاستماع لرسائل MQTT من أجهزة IoT.
  • التقييم: الأكثر كفاءة للأحداث الفورية، ولكنه أكثر تعقيداً في التنفيذ.

4. Internal Event Trigger

  • آلية العمل: يستمع لأحداث داخلية في النظام الذي يعمل عليه n8n.
  • الحالة (State): يعتمد على نوع الحدث.
  • مثال: مراقبة إنشاء ملف جديد في مجلد معين (File System Events)، أو الاستماع لرسائل من Message Queue على نفس الشبكة.
  • التقييم: قوي جداً للتكاملات العميقة على مستوى البنية التحتية.

في هذا المقال، سنركز على بناء Polling Trigger حقيقي وقابل للتوسعة، لأنه يجمع بين الفائدة العملية والحاجة لإدارة الحالة، مما يجعله مثالاً تعليمياً ممتازاً.

ثالثاً: تجهيز بيئة التطوير

قبل الغوص في الكود، نحتاج لتجهيز “العدة”.

المتطلبات:

  • Node.js (يفضل نسخة 18 أو أحدث)
  • n8n مثبت محلياً (سنقوم باستنساخ الكود المصدري)
  • معرفة أساسية بـ TypeScript

1. استنساخ مستودع n8n:

git clone https://github.com/n8n-io/n8n.git
cd n8n

2. تثبيت الاعتماديات:
n8n يستخدم pnpm لإدارة الحزم. إنه أسرع ويوفر مساحة على القرص.

pnpm install

3. تشغيل بيئة التطوير:

pnpm start

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

ملاحظة هامة: في هذا الدليل، سنضيف الـ Trigger مباشرة داخل الكود المصدري لـ n8n (في مجلد packages/nodes-base/nodes). هذا ليس الأسلوب المتبع لإنشاء “Community Node Package”، ولكنه أفضل أسلوب تعليمي لأنه يكشف لنا كل الطبقات الداخلية وكيفية تفاعل الـ Trigger مع نظام n8n الأساسي.

رابعاً: بنية Node في n8n (تشريح داخلي)

أي Node في n8n، سواء كان عادياً أو Trigger، هو عبارة عن ملف TypeScript يعرّف Class تتبع واجهة (Interface) محددة. بشكل أساسي، يتكون من:

MyNode.node.ts
└── class MyNode implements INodeType
    ├── description  // بيانات وصفية للـ Node (الاسم، الأيقونة، الحقول...)
    └── methods      // يحتوي على منطق التنفيذ
        └── execute() // الدالة التي تُنفذ عند وصول البيانات للـ Node

لكن، الـ Trigger Node يضيف مفاهيم إضافية ويختلف قليلاً:

  • يستخدم دالة trigger() بدلاً من execute().
  • دالة trigger() يجب أن تُرجع كائناً يحتوي على closeFunction.
  • يملك صلاحية الوصول إلى دالة this.emit() لإطلاق تنفيذ الـ Workflow.

خامساً: إنشاء Trigger Node من الصفر

هيا بنا نبدأ العمل الفعلي. سننشئ Trigger بسيطاً يقوم بفحص Endpoint URL معين كل 10 ثوانٍ، وإذا وجد بيانات جديدة، يقوم بتشغيل الـ Workflow.

1. إنشاء الملف

اذهب إلى المسار packages/nodes-base/nodes وأنشئ مجلداً جديداً باسم MyCustomTrigger. بداخله، أنشئ ملفاً باسم MyCustomTrigger.node.ts.

في بداية الملف، استورد الواجهات الضرورية من n8n:

// packages/nodes-base/nodes/MyCustomTrigger/MyCustomTrigger.node.ts
import {
	ITriggerFunctions,
	ITriggerResponse,
	INodeType,
	INodeTypeDescription,
} from 'n8n-workflow';

2. تعريف الـ Node Description

الـ description هو “بطاقة الهوية” للـ Node. هو ما يحدد كيف سيظهر في واجهة n8n، ما هي الحقول التي سيحتويها، وما هو اسمه.

أضف الكود التالي في ملفك:

export class MyCustomTrigger implements INodeType {
	description: INodeTypeDescription = {
		displayName: 'My Custom Trigger',
		name: 'myCustomTrigger',
		// الأيقونة يمكن إضافتها هنا
		// icon: 'file:myCustomTrigger.svg',
		group: ['trigger'],
		version: 1,
		description: 'A custom polling trigger built from scratch',
		defaults: {
			name: 'My Custom Trigger',
		},
		inputs: [], // فارغة لأنه Trigger
		outputs: ['main'], // يملك مخرجاً واحداً
		properties: [
			// هنا نعرّف الحقول التي ستظهر للمستخدم في الواجهة
			{
				displayName: 'Endpoint URL',
				name: 'endpoint',
				type: 'string',
				default: '',
				placeholder: 'https://api.example.com/events',
				description: 'The URL to poll for new data',
				required: true,
			},
			{
				displayName: 'Polling Interval (Seconds)',
				name: 'interval',
				type: 'number',
				default: 60,
				description: 'How often to check for new data',
			},
		],
	};

    // ... دالة trigger() ستأتي هنا
}

لاحظ النقاط المهمة:

  • group: ['trigger']: هذا السطر يخبر n8n أن هذا الـ Node هو من نوع Trigger، وسيظهر تحت قائمة الـ Triggers.
  • inputs: []: هذا يؤكد أنه نقطة بداية ولا يستقبل بيانات من Nodes أخرى.
  • properties: مصفوفة تصف كل حقل سيظهر في واجهة المستخدم. هنا أضفنا حقلين: واحد للـ URL والآخر لتحديد الفاصل الزمني للفحص.

سادساً: قلب الـ Trigger — دالة ()trigger

هذه هي الدالة التي تحتوي على المنطق الفعلي للـ Trigger. يتم استدعاؤها مرة واحدة فقط عندما يتم تفعيل الـ Workflow.

أضف هذه الدالة داخل الـ Class:

async trigger(this: ITriggerFunctions): Promise<ITriggerResponse> {
	// 1. الحصول على الإعدادات من واجهة المستخدم
	const endpoint = this.getNodeParameter('endpoint', '') as string;
	const interval = this.getNodeParameter('interval', 60) as number;

	// متغير لتخزين مؤقت الـ-setInterval
	let timer: NodeJS.Timeout;

	// 2. تعريف دالة الفحص الدوري (Polling)
	const poll = async () => {
		try {
			console.log('Polling for new data from:', endpoint);
			const response = await this.helpers.httpRequest({ url: endpoint });
			const data = typeof response === 'string' ? JSON.parse(response) : response;

			// 3. التحقق من وجود بيانات لإرسالها
			// هذا منطق بسيط، في الواقع قد يكون أعقد
			if (data && data.length > 0) {
				console.log(`Found ${data.length} new items. Emitting...`);
				// 4. إطلاق تنفيذ الـ Workflow
				this.emit([
					this.helpers.returnJsonArray(data)
				]);
			}
		} catch (error) {
			console.error("Error during polling:", error);
		}
	};

	// 5. بدء عملية الفحص الدوري عند تفعيل الـ Trigger
	// نضرب القيمة بالثواني في 1000 لتحويلها إلى ميلي ثانية
	timer = setInterval(poll, interval * 1000);

	// 6. إرجاع دالة الإغلاق (Cleanup)
	return {
		closeFunction: async () => {
			console.log('Closing trigger and clearing interval.');
			clearInterval(timer);
		},
	};
}

ماذا يحدث هنا فعلياً؟ (تحليل خطوة بخطوة)

  1. الحصول على الإعدادات: نستخدم this.getNodeParameter() لقراءة القيم التي أدخلها المستخدم في الواجهة (الـ URL والفاصل الزمني).
  2. دالة poll: هذه هي الدالة التي تقوم بالعمل الحقيقي. تستخدم this.helpers.httpRequest (مساعد مدمج في n8n) لجلب البيانات من الـ Endpoint.
  3. التحقق من البيانات: نقوم بفحص بسيط للتأكد من أننا استلمنا بيانات.
  4. this.emit(): هذه هي “الزناد” الحقيقي. هذه الدالة تخبر n8n: “لقد وجدت حدثاً جديداً، ابدأ تنفيذ الـ Workflow الآن”. البيانات التي تمررها إلى emit ستكون هي المدخلات للـ Node التالي في الـ Workflow. نستخدم this.helpers.returnJsonArray() لضمان أن البيانات مهيأة بالشكل الصحيح الذي يتوقعه n8n.
  5. setInterval(poll, ...): هذه هي آلية التكرار. نخبر Node.js أن يقوم بتنفيذ دالة poll كل فترة زمنية محددة.
  6. closeFunction: هذه الدالة حاسمة جداً! عندما يقوم المستخدم بإلغاء تفعيل الـ Workflow، يستدعي n8n هذه الدالة. مهمتها هي “تنظيف” أي عمليات مستمرة. هنا، نستخدمها لإيقاف الـ setInterval عبر clearInterval. إذا نسيت هذه الخطوة، سيبقى الـ Trigger يعمل في الخلفية إلى الأبد، مسبباً تسرباً في الذاكرة (Memory Leak).

سابعاً: إدارة الحالة (State Management)

الـ Trigger الذي بنيناه يعمل، لكنه يفتقر لميزة أساسية: إنه “أبله”. في كل مرة يقوم بالفحص، سيجلب نفس البيانات ويرسلها مراراً وتكراراً. إذا كان الـ API يرجع آخر 10 أحداث، فسيقوم الـ Trigger بتشغيل الـ Workflow بنفس الـ 10 أحداث كل دقيقة!

الحل هو إدارة الحالة: يجب على الـ Trigger أن “يتذكر” آخر حدث قام بمعالجته.

n8n يوفر آلية مدمجة ورائعة لهذا الغرض: this.getWorkflowStaticData('node'). هذا الكائن يتم تخزينه بشكل دائم ويكون مرتبطاً بهذا الـ Node تحديداً داخل هذا الـ Workflow.

دعنا نعدل دالة poll لتصبح أذكى:

// داخل دالة trigger، سنعدل دالة poll
const poll = async () => {
	try {
		// 1. الحصول على البيانات المخزنة سابقاً
		const staticData = this.getWorkflowStaticData('node');

		// 2. إضافة متغير لتتبع آخر ID تمت معالجته
		// إذا لم يكن موجوداً، نبدأ من 0
		if (staticData.lastId === undefined) {
			staticData.lastId = 0;
		}

		console.log(`Polling for new data from: ${endpoint}, checking for items newer than ID: ${staticData.lastId}`);
		// لنفترض أن الـ API يدعم بارامتر `since`
		const response = await this.helpers.httpRequest({ url: `${endpoint}?since=${staticData.lastId}` });
		const data = typeof response === 'string' ? JSON.parse(response) : response;

		if (data && data.length > 0) {
			// 3. فلترة البيانات التي تمت معالجتها سابقاً (كحماية إضافية)
			const newItems = data.filter((item: any) => item.id > staticData.lastId);

			if (newItems.length > 0) {
				console.log(`Found ${newItems.length} new items. Emitting...`);
				this.emit([
					this.helpers.returnJsonArray(newItems)
				]);

				// 4. تحديث الحالة بآخر ID جديد
				// نجد أكبر ID في البيانات الجديدة ونخزنه
				const maxId = Math.max(...newItems.map((item: any) => item.id));
				staticData.lastId = maxId;
			}
		}
	} catch (error) {
		console.error("Error during polling:", error);
	}
};

الآن، أصبح الـ Trigger ذكياً. في كل مرة يعمل، يعرف من أين يبدأ، ويتجنب تكرار العمل. هذه هي الزبدة في بناء أي Polling Trigger احترافي.

ثامناً: أخطاء شائعة (مهم جداً!)

خلال رحلتك في بناء الـ Triggers، ستقع في بعض هذه الأخطاء. اسمح لي أن أختصر عليك الطريق:

  • استخدام execute() بدل trigger(): الـ execute() مخصص للـ Nodes العادية. n8n لن يتعرف على الـ Trigger الخاص بك إذا لم تستخدم الدالة الصحيحة.
  • نسيان closeFunction: هذا هو الخطأ الأخطر. سيؤدي إلى “Triggers زومبي” تستهلك موارد السيرفر حتى بعد إيقاف الـ Workflow، وقد يؤدي إلى انهيار خدمة n8n بالكامل.
  • Polling بدون تحكم في التكرار (Rate Control): إذا قمت بوضع فاصل زمني صغير جداً (مثلاً، كل ثانية)، قد تتسبب في حظر الـ IP الخاص بك من قبل الـ API الذي تستخدمه. كن عاقلاً في اختيار الفاصل الزمني.
  • emit داخل حلقة for غير محمية: إذا كان الـ API يرجع 1000 عنصر، وقمت بعمل emit لكل عنصر على حدة داخل حلقة، ستطلق 1000 تنفيذ للـ Workflow في نفس اللحظة! هذا يسمى “Fan-out” وهو خطير جداً على أداء النظام. الحل هو إرسال البيانات كدفعة واحدة كما فعلنا: this.emit([this.helpers.returnJsonArray(data)]).

تاسعاً: الفرق بين Trigger حقيقي و “حل ترقيعي”

قد يقول قائل: “لماذا كل هذا التعقيد؟ يمكنني استخدام Cron Node يعمل كل دقيقة وينفذ HTTP Request Node”. نعم، يمكنك، ولكنك تبني حلاً هشاً. إليك الفرق:

الجانب Trigger حقيقي (ما بنيناه) حل ترقيعي (Cron + HTTP)
دورة الحياة (Lifecycle) تدار بواسطة n8n (Activate/Deactivate) مكسورة (لا يوجد إيقاف حقيقي للعمليات)
إدارة الحالة (State) مدمجة وآمنة (staticData) صعبة، تتطلب تخزين الحالة في ملف أو DB خارجي
التوسع (Scaling) آمن، مصمم للعمل في بيئات متعددة الـ workers خطير، قد تحدث تكرارات ومشاكل في التزامن
الموثوقية عالية، مصمم للإنتاج منخفضة، مناسب للتجارب السريعة فقط
بيئة الإنتاج ✔ جاهز ✖ لا ينصح به أبداً

عاشراً: متى تبني Trigger بنفسك؟

الخلاصة، لا تذهب لهذا الطريق إلا عند الحاجة الحقيقية.

ابنِ Trigger مخصص إذا:

  • لديك مصدر أحداث (Event Source) غير مدعوم نهائياً بأي Trigger جاهز.
  • تريد تحكماً كاملاً في منطق التشغيل وإدارة الحالة.
  • تعمل على بناء منصة (Platform) أو خدمة SaaS فوق n8n وتحتاج تكاملاً عميقاً.

ولا تبنه إذا:

  • Webhook يفي بالغرض. دائماً اختر الحل الأبسط.
  • الحل مؤقت أو لمشروع شخصي صغير، وحل “الترقيع” باستخدام Cron مقبول للمخاطر.

خاتمة: أنت الآن تفهم n8n من الداخل

بناء Trigger مخصص ليس مجرد إضافة ميزة… بل هو رحلة لفهم معماري عميق لكيفية عمل n8n. هو الانتقال من كونك “مستهلكاً” للأداة إلى “مشارك” في بنائها وتوسيعها.

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

وهنا، يا جماعة الخير، تبدأ المتعة الحقيقية في عالم البرمجة والأتمتة. 🚀

الكود الذي كتبناه اليوم هو الأساس. يمكن تطوير هذا الـ Trigger ليصبح حزمة مستقلة (Custom Node Package) أو حتى تكامل (Integration) كاملة يتم إضافتها لـ n8n. لكن الأساس الذي قرأته هنا هو القلب الحقيقي للنظام.

بالتوفيق في رحلتكم!

أبو عمر

سجل دخولك لعمل نقاش تفاعلي

كافة المحادثات خاصة ولا يتم عرضها على الموقع نهائياً

آراء من النقاشات

لا توجد آراء منشورة بعد. كن أول من يشارك رأيه!

آخر المدونات

أتمتة العمليات

قهوتك الصباحية مع ملخص الإنجازات: كيف تبني داشبورد يومي يصلك على الموبايل باستخدام n8n والذكاء الاصطناعي

كف عن تشتيت نفسك كل صباح بين Jira وGitHub والإيميلات. تعلم معي، أبو عمر، كيف تبني ورك فلو أتمتة يرسل لك ملخصاً ذكياً ومنسقاً بإنجازات...

12 فبراير، 2026 قراءة المزيد
البودكاست