تحتاج معظم تطبيقات الإنتاج إلى عمل لا يتناسب مع دورة الطلب/الاستجابة: إرسال رسائل البريد الإلكتروني ومعالجة التحميلات وتشغيل خطوط أنابيب الذكاء الاصطناعي ومزامنة بيانات الطرف الثالث وإنشاء التقارير. الإجابة التقليدية هي قائمة انتظار (Redis، SQS، RabbitMQ)، أسطول من العمال، مجدول، وكومة هشة من رمز الترابط الذي ينكسر في كل عملية نشر.
Trigger.dev يطوي تلك الحزمة في SDK واحد TypeScript. تكتب الدوال، وتستدعيها من أي مكان، وتتعامل المنصة مع الطوابير وإعادة المحاولات والمراقبة والجدولة والتنفيذ الدائم. تعمل المهام لأطول فترة تحتاجها - لا توجد مهلة بدون خادم لمدة 10 ثوانٍ، ولا عمل ضائع عند إعادة النشر.
لماذا Trigger.dev
التحول في 2026 هو التنفيذ الدائم. يجب أن تنجو سير العمل من إعادة التشغيل والأعطال وعمليات النشر وحدود المعدل. يجب عليهم أيضًا دفق التقدم إلى واجهة المستخدم في الوقت الفعلي والإيقاف المؤقت لإدخال الإنسان. تم إعادة بناء Trigger.dev حول هذه المتطلبات مع الإصدار 3 ويواصل توسيع سطح البنية التحتية للذكاء الاصطناعي الخاص به.
النموذج بسيط: تحدد المهام كصادرات، يلتقطها SDK، تجدول المنصة وتشغلها في حاويات معزولة، ويتم الاحتفاظ بحالة التشغيل حتى تتمكن من الاستئناف وإعادة المحاولة والمراقبة.
البدء
تهيئة مشروع
npx trigger.dev@latest login npx trigger.dev@latest init
هذا ينشئ ملف trigger.config.ts ودليل trigger/ مع مهام مثال. ملف التكوين هو مصدر الحقيقة لمشروعك: ما هي الأدلة التي تحتوي على مهام، وإعدادات البناء، وخطافات دورة الحياة، وخيارات وقت التشغيل.
// trigger.config.ts import { defineConfig } from "@trigger.dev/sdk"; export default defineConfig({ project: "proj_abc123", runtime: "node", logLevel: "log", maxDuration: 3600, retries: { enabledInDev: true, default: { maxAttempts: 3, factor: 2, minTimeoutInMs: 1000, maxTimeoutInMs: 30_000, }, }, dirs: ["./trigger"], });
تشغيل المهام محليًا
npx trigger.dev@latest dev
يتصل خادم dev بالسحابة، ويسجل مهامك، ويدفق التشغيلات من خلال رمزك المحلي. تضع نقاط توقف في محررك وتصيبها على المشغلات الحقيقية - نفس الحلقة التي ستستخدمها في أي مشروع Node.js عادي.
تعريف Task
المهمة هي كائن يتم تصديره بمعرف id فريد ودالة run. يفحص SDK الصادرات عبر dirs ويسجلها تلقائيًا.
// trigger/send-welcome-email.ts import { task } from "@trigger.dev/sdk"; import { Resend } from "resend"; const resend = new Resend(process.env.RESEND_API_KEY); export const sendWelcomeEmail = task({ id: "send-welcome-email", retry: { maxAttempts: 5, factor: 1.8, minTimeoutInMs: 500, maxTimeoutInMs: 30_000, }, run: async (payload: { email: string; name: string }) => { const { data, error } = await resend.emails.send({ from: "hello@spinny.dev", to: payload.email, subject: `Welcome, ${payload.name}`, html: `<p>Glad you are here, ${payload.name}.</p>`, }); if (error) throw error; return { messageId: data?.id }; }, });
ثلاثة أشياء يجب ملاحظتها:
- لا توجد مهلة في جسم run. تدير المنصة وقت التنفيذ عبر
maxDurationفي التكوين، وليس وقت التشغيل. - الإلقاءات هي إعادة محاولات. يلتقط SDK الاستثناءات ويعيد التشغيل مع تراجع أسي وفقًا لسياسة
retry. - يتم الاحتفاظ بقيمة الإرجاع. يمكن للمهام الأخرى وواجهتك الأمامية قراءة
run.outputمن أي مكان.
تشغيل المهام
تستدعي مهمة من الواجهة الخلفية أو مسارات API الخاصة بك أو مهمة أخرى.
import { sendWelcomeEmail } from "@/trigger/send-welcome-email"; const handle = await sendWelcomeEmail.trigger( { email: "user@example.com", name: "Alex" }, { idempotencyKey: `welcome-${userId}`, concurrencyKey: `tenant-${tenantId}`, queue: { name: "emails", concurrencyLimit: 50 }, delay: "30s", ttl: "10m", } ); console.log(handle.id); // run_xyz - استخدم هذا لتتبع أو عرض التقدم
تفتح الخيارات الكثير من السلوك في مكالمة واحدة:
idempotencyKey- إذا كان التشغيل بنفس المفتاح موجودًا بالفعل، فإن SDK يعيد المقبض الموجود بدلاً من تكرار العمل.concurrencyKey- يسلسل عمليات التشغيل التي تشترك في المفتاح حتى لا تتجاوز حد المعدل لكل مستأجر.queue.concurrencyLimit- الحد الأقصى العالمي للطابور عبر جميع المفاتيح.delay- يجدول التشغيل لوقت مستقبلي.ttl- إذا لم يبدأ التشغيل بحلول ذلك الوقت، فقم بانتهاء صلاحيته تلقائيًا.
Batch trigger
لأعباء العمل المتفرعة، يقبل batchTrigger ما يصل إلى 500 عنصر لكل مكالمة وينشئ تشغيلًا واحدًا لكل عنصر.
await sendWelcomeEmail.batchTrigger( newUsers.map((u) => ({ payload: { email: u.email, name: u.name }, options: { idempotencyKey: `welcome-${u.id}` }, })) );
المهام المجدولة
تصبح وظائف Cron إعلانات من الدرجة الأولى. الجدول الزمني نفسه هو كائن منفصل يمكنك إرفاقه بمهمة عدة مرات.
// trigger/daily-digest.ts import { schedules } from "@trigger.dev/sdk"; export const dailyDigest = schedules.task({ id: "daily-digest", cron: "0 9 * * *", run: async (payload) => { console.log("Scheduled at:", payload.timestamp); console.log("Last run:", payload.lastTimestamp); console.log("Timezone:", payload.timezone); console.log("Next 5 runs:", payload.upcoming); await sendDigestForDate(payload.timestamp); }, });
للجداول الزمنية لكل مستأجر - لنقل، cron واحد لكل عميل - تقوم بإنشائها ديناميكيًا من خلال management API.
import { schedules } from "@trigger.dev/sdk"; await schedules.create({ task: "daily-digest", cron: "0 9 * * *", timezone: "America/New_York", externalId: `customer_${customerId}`, deduplicationKey: `digest-${customerId}`, });
deduplicationKey يجعل الاستدعاء idempotent: إعادة تشغيل نفس الرمز في وقت النشر لا يكدس الجداول الزمنية المكررة.
الطوابير والتزامن والإمكانية المتكررة
ثلاثة بدائيات تغطي معظم احتياجات الحد من المعدل والترتيب.
نمط شائع: طابور لكل مستأجر مع تزامن صغير لكل مفتاح لاحترام حد معدل البائع، بالإضافة إلى مفتاح الإمكانية المتكررة لجعل إعادة المحاولات آمنة.
await syncShopifyOrders.trigger( { shopId }, { queue: { name: `shopify-${shopId}`, concurrencyLimit: 2 }, concurrencyKey: shopId, idempotencyKey: `sync-${shopId}-${Date.now() / 60_000 | 0}`, } );
الانتظار والعمل طويل الأمد
يمكن للمهام أن تتوقف مؤقتًا دون الاحتفاظ باتصال أو حرق الحوسبة. تحتفظ المنصة بالحالة وتستأنف الوظيفة عند اكتمال الانتظار.
import { wait } from "@trigger.dev/sdk"; export const onboarding = task({ id: "onboarding", run: async (payload: { userId: string }) => { await sendWelcomeEmail.triggerAndWait({ userId: payload.userId }); await wait.for({ days: 1 }); await sendTipsEmail.trigger({ userId: payload.userId }); await wait.until({ date: oneWeekFromSignup(payload.userId) }); await sendUpgradeOffer.trigger({ userId: payload.userId }); }, });
triggerAndWait هي الميزة القاتلة: تطلق مهمة فرعية وتعلق الأصل حتى تكتمل العملية الفرعية. تكتب المهام مثل دوال async، لكن التنسيق يعمل بشكل دائم عبر أيام أو أسابيع.
Human-in-the-loop مع wait.forToken
لتدفقات الموافقة وبوابات الذكاء الاصطناعي، يتوقف wait.forToken مؤقتًا حتى يقوم تطبيقك بالاستدعاء بنتيجة.
import { task, wait } from "@trigger.dev/sdk"; export const publishPost = task({ id: "publish-post", run: async (payload: { draftId: string }) => { const draft = await generateAIContent(payload.draftId); const token = await wait.createToken({ timeout: "7d" }); await notifyEditor({ draftId: draft.id, token: token.id }); const decision = await wait.forToken<{ approved: boolean; notes?: string }>( token.id ); if (decision.approved) { return await publish(draft); } return await applyFeedback(draft, decision.notes); }, });
يفتح المحرر واجهة، ويراجع المسودة، وينقر فوق موافقة، ويكمل الواجهة الخلفية الرمز المميز. تستأنف المهمة من حيث توقفت - حتى لو مرت ساعات أو أيام.
خطافات دورة الحياة
يمكنك إرفاق init و onStart و onSuccess و onFailure بمهمة أو على مستوى عالمي في trigger.config.ts. استخدمها للتتبع والإبلاغ عن الأخطاء والإعداد المشترك.
// trigger.config.ts export default defineConfig({ // ... init: async () => { Sentry.init({ dsn: process.env.SENTRY_DSN }); }, onFailure: async ({ error, ctx }) => { Sentry.captureException(error, { tags: { taskId: ctx.task.id, runId: ctx.run.id }, }); }, });
init يعمل مرة واحدة لكل حاوية عاملة عند التمهيد، وليس لكل تشغيل، لذا فهو المكان الصحيح لإعداد العملاء والتجمعات.
الوقت الفعلي في الواجهة الأمامية
ينشر Trigger.dev تغييرات حالة التشغيل - الحالة، البيانات الوصفية، الإخراج - عبر API دفق. تشترك خطافات React في هذا الدفق وتعيد العرض تلقائيًا.
// trigger/process-video.ts import { task, metadata } from "@trigger.dev/sdk"; export const processVideo = task({ id: "process-video", run: async (payload: { videoId: string }) => { metadata.set("stage", "transcoding"); await transcode(payload.videoId); metadata.set("stage", "thumbnails"); await generateThumbnails(payload.videoId); metadata.set("stage", "uploading"); const url = await uploadToCDN(payload.videoId); return { url }; }, });
// components/VideoStatus.tsx "use client"; import { useRealtimeRun } from "@trigger.dev/react-hooks"; import type { processVideo } from "@/trigger/process-video"; export function VideoStatus({ runId, publicAccessToken, }: { runId: string; publicAccessToken: string; }) { const { run, error } = useRealtimeRun<typeof processVideo>(runId, { accessToken: publicAccessToken, }); if (error) return <p>Error: {error.message}</p>; if (!run) return <p>Loading...</p>; return ( <div> <p>Status: {run.status}</p> <p>Stage: {String(run.metadata?.stage ?? "queued")}</p> {run.output?.url && <video src={run.output.url} controls />} </div> ); }
تقوم بإنشاء رمز الوصول العام من جانب الخادم، مع نطاق محدد لتشغيل معين، وتشحنه إلى العميل. يتعامل الخطاف مع المصادقة وإعادة الاتصال والتحديثات التزايدية.
للتشغيل والاشتراك في خطوة واحدة:
import { useRealtimeTaskTrigger } from "@trigger.dev/react-hooks"; const { submit, run, isLoading } = useRealtimeTaskTrigger<typeof processVideo>( "process-video", { accessToken: publicAccessToken } ); <button onClick={() => submit({ videoId })} disabled={isLoading}> Process video </button>;
وكلاء الذكاء الاصطناعي والدفق
أصبح Trigger.dev وقت تشغيل شائع لوكلاء الذكاء الاصطناعي لأن نفس البدائيات - التنفيذ الدائم، إعادة المحاولات، الانتظار، البيانات الوصفية في الوقت الفعلي، human-in-the-loop - هي بالضبط ما يحتاجه الوكلاء. تقوم بدفق الرموز من مزود نموذج إلى metadata بينما يحدث التشغيل، تقدمها الواجهة الأمامية مباشرة، ويبقى التشغيل على قيد الحياة من خلال استدعاءات الأدوات طويلة المدى دون حرق مهلة بدون خادم.
import { task, metadata } from "@trigger.dev/sdk"; import { streamText } from "ai"; import { anthropic } from "@ai-sdk/anthropic"; export const researchAgent = task({ id: "research-agent", maxDuration: 1800, run: async (payload: { question: string }) => { const result = streamText({ model: anthropic("claude-opus-4-7"), system: "You are a research assistant. Use the web.", prompt: payload.question, tools: { webSearch }, }); let fullText = ""; for await (const chunk of result.textStream) { fullText += chunk; metadata.set("partial", fullText); } return { answer: fullText, usage: await result.usage }; }, });
تستخدم الواجهة الأمامية useRealtimeRun وتقرأ run.metadata.partial لتقديم الاستجابة المتدفقة، بنفس الطريقة التي ستقدم بها إكمال محادثة - باستثناء أن هذا يبقى على قيد الحياة من خلال إعادة تحميل الصفحة بالكامل.
النشر
تقوم عمليات النشر بترجمة مهامك إلى حزمة مُصدارة، وبناء حاوية، وتبديل حركة المرور بشكل ذري. تستمر عمليات التشغيل القديمة قيد التنفيذ في استخدام الإصدار السابق.
npx trigger.dev@latest deploy --env prod
في CI، عادةً ما تربط هذا في نفس سير العمل الذي يشحن تطبيقك:
# .github/workflows/deploy.yml - name: Deploy Trigger.dev env: TRIGGER_ACCESS_TOKEN: ${{ secrets.TRIGGER_ACCESS_TOKEN }} run: npx trigger.dev@latest deploy --env prod
لبيئات المعاينة، قم بتمرير --env preview --branch ${{ github.head_ref }} ويقوم Trigger.dev بإنشاء بيئة معزولة لكل فرع، مما يعكس كيفية تعامل Vercel مع عمليات نشر المعاينة.
الاستضافة الذاتية مقابل السحابة
Trigger.dev مفتوح المصدر بموجب ترخيص Apache 2.0. يمكنك استضافة ذاتية على أي منصة حاوية (Docker Compose، Kubernetes، Fly.io) أو استخدام السحابة المدارة على trigger.dev.
| الجانب | السحابة | الاستضافة الذاتية |
|---|---|---|
| الإعداد | الاشتراك، تشغيل init | تشغيل docker-compose أو Helm chart |
| التحجيم | تلقائي | مسؤوليتك |
| التسعير | لكل تشغيل + لكل حساب | تكلفة البنية التحتية فقط |
| الامتثال | SOC 2 | ما توفره بيئتك |
| الأنسب لـ | معظم الفرق | إقامة بيانات صارمة، بنية تحتية مخصصة |
SDK و CLI متطابقان بين الأوضاع - تقوم بتغيير علامة ملف التعريف وتشير إلى مثيلك الخاص.
أفضل الممارسات
1. حافظ على حمولات صغيرة وقابلة للتسلسل
قم بتمرير المعرفات والمراجع، وليس الكائنات الكاملة. اسحب البيانات داخل المهمة. هذا يحافظ على الطابور صغيرًا، والحمولات رخيصة للتسجيل، ويتيح لك تغيير مصدر البيانات دون إعادة التشغيل.
2. مفاتيح Idempotency على كل استدعاء خارجي
اجمع idempotencyKey على مشغل المهمة مع مفاتيح idempotency في واجهات API للبائعين (Stripe، OpenAI، إلخ). ستكون إعادة المحاولات آمنة من البداية إلى النهاية.
3. استخدم triggerAndWait للتنسيق، وليس Promise.all من المشغلات
أحد الوالدين الذي يستدعي triggerAndWait يؤلف بشكل دائم مهام فرعية. أحد الوالدين الذي يطلق ويحل على الفور يفقد إمكانية مراقبة السلسلة.
4. ضع علامات على عمليات التشغيل
أضف tags إلى المشغلات (tags: ["user:123", "feature:onboarding"]) حتى تتمكن من تصفية لوحة المعلومات وواجهة management API حسب أبعاد العمل.
5. حافظ على init idempotent
يعمل عند كل بداية باردة. تجنب الترحيلات أو الآثار الجانبية لمرة واحدة هناك.
الخاتمة
يزيل Trigger.dev فئات العمل التي اعتادت أن تتطلب بناء نظام وظائف من الصفر. تكتب async TypeScript، تستدعيها من أي مكان، وتمنحك المنصة التنفيذ الدائم والجدولة والطوابير وإعادة المحاولات وتحديثات الوقت الفعلي وأنماط human-in-the-loop خارج الصندوق.
نفس السطح الذي يشغل cron ليلي هو السطح الذي يشغل وكيل ذكاء اصطناعي متعدد الخطوات يدفق إلى الواجهة الأمامية ويتوقف للمراجعة. هذا التقارب هو ما يجعل الإطار يستحق نظرة جادة في 2026، سواء كنت تدير SaaS يحتاج إلى عمل خلفي موثوق أو تشحن ميزات الذكاء الاصطناعي التي تعيش بعد مهلة بدون خادم.
قائمة التحقق للبدء:
- التسجيل في trigger.dev أو تشغيل حزمة Docker للاستضافة الذاتية
npx trigger.dev@latest initفي مشروعك- حدد مهمتك الأولى باستخدام
task({ id, run })- قم بتشغيلها من API الخاص بك وشاهد التشغيل في لوحة المعلومات
- أضف
idempotencyKeyوconcurrencyKeyلأمان الإنتاج- قم بتوصيل
useRealtimeRunبمكون الحالة- انشر باستخدام
trigger.dev deploy --env prodمن CI