اکثر برنامههای production به کاری نیاز دارند که در چرخه request/response جا نمیگیرد: ارسال ایمیل، پردازش آپلودها، اجرای pipelineهای AI، همگامسازی دادههای شخص ثالث، تولید گزارش. پاسخ سنتی یک صف (Redis, SQS, RabbitMQ)، یک ناوگان worker، یک scheduler و یک پشته شکننده از کد چسب است که در هر deploy میشکند.
Trigger.dev آن stack را در یک TypeScript SDK واحد فشرده میکند. شما توابع را مینویسید، آنها را از هر جایی فراخوانی میکنید و پلتفرم queueing، retries، observability، scheduling و اجرای پایدار را مدیریت میکند. Tasks تا زمانی که نیاز است اجرا میشوند - بدون timeout serverless 10 ثانیهای، بدون کار از دست رفته در redeploy.
چرا Trigger.dev
تغییر در 2026 اجرای پایدار است. Workflows باید از restart، crash، deploy و rate limit جان سالم به در ببرند. آنها همچنین باید پیشرفت را به صورت realtime به UI استریم کنند و برای ورودی انسانی متوقف شوند. Trigger.dev در نسخه 3 حول این الزامات بازسازی شد و به گسترش سطح زیرساخت AI خود ادامه میدهد.
مدل ساده است: tasks را به عنوان exports تعریف میکنید، SDK آنها را برمیدارد، پلتفرم آنها را برنامهریزی و در کانتینرهای ایزوله اجرا میکند و وضعیت run حفظ میشود تا بتوانید ادامه دهید، دوباره تلاش کنید و مشاهده کنید.
شروع
یک پروژه را مقداردهی اولیه کنید
npx trigger.dev@latest login npx trigger.dev@latest init
این یک فایل trigger.config.ts و یک دایرکتوری trigger/ با tasks نمونه ایجاد میکند. فایل config منبع حقیقت برای پروژه شماست: کدام دایرکتوریها حاوی tasks هستند، تنظیمات build، lifecycle hooks و گزینههای runtime.
// 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"], });
tasks را به صورت محلی اجرا کنید
npx trigger.dev@latest dev
سرور dev به cloud متصل میشود، tasks شما را ثبت میکند و runs را از طریق کد محلی شما استریم میکند. در ویرایشگر خود breakpoint میگذارید و آنها را روی triggerهای واقعی میزنید - همان loop که در هر پروژه عادی Node.js استفاده میکنید.
تعریف یک Task
Task یک شیء است که با id منحصر به فرد و یک تابع run صادر میشود. SDK exports را در 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 }; }, });
سه نکته که باید توجه کنید:
- بدون timeout در بدنه run. پلتفرم زمان اجرا را از طریق
maxDurationدر config مدیریت میکند، نه در runtime. - Throwsها retries هستند. SDK exceptionها را میگیرد و طبق سیاست
retryبا backoff نمایی دوباره اجرا میکند. - مقدار بازگشتی حفظ میشود. سایر tasks و frontend شما میتوانند
run.outputرا از هر جایی بخوانند.
Triggering Tasks
شما task را از backend خود، routeهای API یا task دیگر فراخوانی میکنید.
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- اگر run با همان کلید قبلاً وجود داشته باشد، SDK handle موجود را بازمیگرداند به جای تکرار کار.concurrencyKey- runs که کلید را به اشتراک میگذارند را سریالی میکند تا از rate limit per-tenant فراتر نروید.queue.concurrencyLimit- cap جهانی برای queue در همه کلیدها.delay- run را برای زمان آینده برنامهریزی میکند.ttl- اگر run تا آن زمان شروع نشده باشد، به طور خودکار منقضی میشود.
Batch trigger
برای workloadهای fan-out، batchTrigger تا 500 آیتم در هر فراخوانی میپذیرد و یک run در هر آیتم ایجاد میکند.
await sendWelcomeEmail.batchTrigger( newUsers.map((u) => ({ payload: { email: u.email, name: u.name }, options: { idempotencyKey: `welcome-${u.id}` }, })) );
Tasks زمانبندی شده
Cron jobها به اعلانهای کلاس اول تبدیل میشوند. خود schedule یک شیء جداگانه است که میتوانید چندین بار به یک task متصل کنید.
// 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); }, });
برای scheduleهای per-tenant - بگوییم یک 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 میکند: اجرای مجدد همان کد در زمان deploy scheduleهای تکراری را روی هم انباشته نمیکند.
Queues, Concurrency و Idempotency
سه primitive اکثر نیازهای rate-limiting و ordering را پوشش میدهند.
یک الگوی رایج: یک queue به ازای هر tenant با concurrency کوچک per-key برای رعایت rate limit یک vendor، به علاوه یک کلید idempotency برای ایمن کردن retries.
await syncShopifyOrders.trigger( { shopId }, { queue: { name: `shopify-${shopId}`, concurrencyLimit: 2 }, concurrencyKey: shopId, idempotencyKey: `sync-${shopId}-${Date.now() / 60_000 | 0}`, } );
Waits و کار طولانیمدت
Tasks میتوانند بدون نگه داشتن اتصال یا سوزاندن compute متوقف شوند. پلتفرم state را حفظ میکند و وقتی wait کامل شد تابع را از سر میگیرد.
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 ویژگی کشنده است: یک child task را trigger میکند و parent را تا زمانی که child تمام شود معلق میکند. tasks را مانند توابع async مینویسید، اما orchestration به طور پایدار در طول روزها یا هفتهها اجرا میشود.
Human-in-the-loop با wait.forToken
برای جریانهای تأیید و gateهای AI، wait.forToken متوقف میشود تا برنامه شما با یک نتیجه callback کند.
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); }, });
ویرایشگر یک UI باز میکند، پیشنویس را بررسی میکند، روی Approve کلیک میکند و backend شما token را تکمیل میکند. task از جایی که متوقف شد ادامه میدهد - حتی اگر ساعتها یا روزها گذشته باشد.
Lifecycle Hooks
میتوانید init, onStart, onSuccess و onFailure را به یک task یا به طور سراسری در trigger.config.ts متصل کنید. از آنها برای tracing, error reporting و setup مشترک استفاده کنید.
// 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 یک بار در هر worker container در boot اجرا میشود، نه در هر run، بنابراین جای مناسبی برای تنظیم clients و poolها است.
Realtime در Frontend
Trigger.dev تغییرات وضعیت run - status, metadata, output - را از طریق streaming API منتشر میکند. React hooks در آن stream مشترک میشوند و به طور خودکار re-render میکنند.
// 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> ); }
شما public access token را در سمت سرور تولید میکنید، scoped به یک run خاص، و آن را به client ارسال میکنید. hook auth، اتصال مجدد و بهروزرسانیهای افزایشی را مدیریت میکند.
برای trigger-and-subscribe در یک قدم:
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>;
AI Agents و Streaming
Trigger.dev به یک runtime محبوب برای AI agents تبدیل شده است زیرا همان primitives - اجرای پایدار، retries، waits، realtime metadata، human-in-the-loop - دقیقاً چیزی است که agents به آن نیاز دارند. tokenها را از یک ارائهدهنده مدل به metadata در حالی که run در حال انجام است stream میکنید، frontend آنها را به صورت زنده render میکند و run بدون سوزاندن timeout serverless از tool callهای طولانیمدت جان سالم به در میبرد.
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 }; }, });
frontend از useRealtimeRun استفاده میکند و run.metadata.partial را برای render پاسخ streaming میخواند، به همان روشی که یک chat completion را render میکردید - با این تفاوت که این یکی از reload کامل صفحه جان سالم به در میبرد.
Deploying
Deployها tasks شما را به یک bundle ورژندار کامپایل میکنند، یک container میسازند و ترافیک را به صورت اتمیک تعویض میکنند. runs قدیمی in-flight به استفاده از نسخه قبلی ادامه میدهند.
npx trigger.dev@latest deploy --env prod
در CI معمولاً این را به همان workflow متصل میکنید که app شما را ارسال میکند:
# .github/workflows/deploy.yml - name: Deploy Trigger.dev env: TRIGGER_ACCESS_TOKEN: ${{ secrets.TRIGGER_ACCESS_TOKEN }} run: npx trigger.dev@latest deploy --env prod
برای محیطهای preview، --env preview --branch ${{ github.head_ref }} را عبور دهید و Trigger.dev یک محیط ایزوله در هر branch ایجاد میکند، که نحوه برخورد Vercel با preview deploymentها را منعکس میکند.
Self-Hosting در مقابل Cloud
Trigger.dev تحت لایسنس Apache 2.0 متنباز است. میتوانید روی هر پلتفرم container (Docker Compose, Kubernetes, Fly.io) self-host کنید یا از cloud مدیریتشده در trigger.dev استفاده کنید.
| جنبه | Cloud | Self-hosted |
|---|---|---|
| Setup | ثبتنام، اجرای init | اجرای docker-compose یا Helm chart |
| Scaling | خودکار | مسئولیت شما |
| Pricing | به ازای run + به ازای compute | فقط هزینه infra |
| Compliance | SOC 2 | هرچه محیط شما فراهم میکند |
| بهترین برای | اکثر تیمها | residency داده سختگیرانه، infra سفارشی |
SDK و CLI بین حالتها یکسان هستند - یک profile flag را تغییر میدهید و به instance خود اشاره میکنید.
Best Practices
1. payloads را کوچک و قابل serialize نگه دارید
IDها و referenceها را عبور دهید، نه اشیاء کامل. دادهها را در داخل task بگیرید. این queue را کوچک نگه میدارد، payloads را برای log کردن ارزان میکند و به شما اجازه میدهد منبع داده را بدون trigger مجدد تغییر دهید.
2. کلیدهای Idempotency در هر فراخوانی خارجی
idempotencyKey در trigger task را با کلیدهای idempotency در APIهای vendor خود (Stripe, OpenAI, و غیره) ترکیب کنید. retriesها end-to-end ایمن خواهند بود.
3. از triggerAndWait برای orchestration استفاده کنید، نه Promise.all از triggerها
یک parent که triggerAndWait را فراخوانی میکند به طور پایدار child tasks را compose میکند. یک parent که trigger میکند و فوراً resolve میشود، observability زنجیره را از دست میدهد.
4. runs را tag کنید
به triggerها tags اضافه کنید (tags: ["user:123", "feature:onboarding"]) تا بتوانید dashboard و management API را بر اساس ابعاد business فیلتر کنید.
5. init را idempotent نگه دارید
در هر cold start اجرا میشود. از migrations یا اثرات جانبی one-shot در آنجا اجتناب کنید.
نتیجهگیری
Trigger.dev دستههای کاری را که قبلاً برای ساختن یک سیستم job از صفر مورد نیاز بودند، حذف میکند. async TypeScript مینویسید، آن را از هر جایی فراخوانی میکنید و پلتفرم به شما اجرای پایدار، scheduling، queues، retries، بهروزرسانیهای realtime و الگوهای human-in-the-loop را به صورت out of the box میدهد.
همان سطحی که یک cron شبانه را قدرت میدهد، سطحی است که یک AI agent چند مرحلهای را قدرت میدهد که به frontend stream میکند و برای review متوقف میشود. این همگرایی است که framework را در 2026 شایسته یک نگاه جدی میکند، چه یک SaaS را اداره میکنید که به کار پسزمینه قابل اعتماد نیاز دارد، چه ویژگیهای AI را ارسال میکنید که از یک serverless timeout جان سالم به در میبرند.
چکلیست شروع:
- در trigger.dev ثبتنام کنید یا stack Docker self-hosted را اجرا کنید
npx trigger.dev@latest initدر پروژه شما- اولین task خود را با
task({ id, run })تعریف کنید- آن را از API خود trigger کنید و run را در dashboard ببینید
- برای امنیت production
idempotencyKeyوconcurrencyKeyاضافه کنیدuseRealtimeRunرا به یک status component متصل کنید- با
trigger.dev deploy --env prodاز CI deploy کنید