spinny:~/writing $ less trigger-dev-background-jobs-guide.md
12اکثر برنامههای production به کاری نیاز دارند که در چرخه request/response جا نمیگیرد: ارسال ایمیل، پردازش آپلودها، اجرای pipelineهای AI، همگامسازی دادههای شخص ثالث، تولید گزارش. پاسخ سنتی یک صف (Redis, SQS, RabbitMQ)، یک ناوگان worker، یک scheduler و یک پشته شکننده از کد چسب است که در هر deploy میشکند.34[Trigger.dev](https://trigger.dev) آن stack را در یک TypeScript SDK واحد فشرده میکند. شما توابع را مینویسید، آنها را از هر جایی فراخوانی میکنید و پلتفرم queueing، retries، observability، scheduling و اجرای پایدار را مدیریت میکند. Tasks تا زمانی که نیاز است اجرا میشوند - بدون timeout serverless 10 ثانیهای، بدون کار از دست رفته در redeploy.56## چرا Trigger.dev78تغییر در 2026 اجرای پایدار است. Workflows باید از restart، crash، deploy و rate limit جان سالم به در ببرند. آنها همچنین باید پیشرفت را به صورت realtime به UI استریم کنند و برای ورودی انسانی متوقف شوند. Trigger.dev در نسخه 3 حول این الزامات بازسازی شد و به گسترش سطح زیرساخت AI خود ادامه میدهد.910```mermaid11graph LR12 App[App شما] -->|trigger| API[Trigger.dev API]13 API --> Queue[Queue پایدار]14 Queue --> Worker[Worker Container]15 Worker -->|run task| Task[کد Task شما]16 Task -->|metadata| Realtime[Stream Realtime]17 Realtime --> UI[React UI]18 Worker --> Storage[Run State Store]19```2021مدل ساده است: tasks را به عنوان exports تعریف میکنید، SDK آنها را برمیدارد، پلتفرم آنها را برنامهریزی و در کانتینرهای ایزوله اجرا میکند و وضعیت run حفظ میشود تا بتوانید ادامه دهید، دوباره تلاش کنید و مشاهده کنید.2223## شروع2425### یک پروژه را مقداردهی اولیه کنید2627```bash28npx trigger.dev@latest login29npx trigger.dev@latest init30```3132این یک فایل `trigger.config.ts` و یک دایرکتوری `trigger/` با tasks نمونه ایجاد میکند. فایل config منبع حقیقت برای پروژه شماست: کدام دایرکتوریها حاوی tasks هستند، تنظیمات build، lifecycle hooks و گزینههای runtime.3334```typescript35// trigger.config.ts36import { defineConfig } from "@trigger.dev/sdk";3738export default defineConfig({39 project: "proj_abc123",40 runtime: "node",41 logLevel: "log",42 maxDuration: 3600,43 retries: {44 enabledInDev: true,45 default: {46 maxAttempts: 3,47 factor: 2,48 minTimeoutInMs: 1000,49 maxTimeoutInMs: 30_000,50 },51 },52 dirs: ["./trigger"],53});54```5556### tasks را به صورت محلی اجرا کنید5758```bash59npx trigger.dev@latest dev60```6162سرور dev به cloud متصل میشود، tasks شما را ثبت میکند و runs را از طریق کد محلی شما استریم میکند. در ویرایشگر خود breakpoint میگذارید و آنها را روی triggerهای واقعی میزنید - همان loop که در هر پروژه عادی Node.js استفاده میکنید.6364## تعریف یک Task6566Task یک شیء است که با `id` منحصر به فرد و یک تابع `run` صادر میشود. SDK exports را در `dirs` بررسی میکند و آنها را به طور خودکار ثبت میکند.6768```typescript69// trigger/send-welcome-email.ts70import { task } from "@trigger.dev/sdk";71import { Resend } from "resend";7273const resend = new Resend(process.env.RESEND_API_KEY);7475export const sendWelcomeEmail = task({76 id: "send-welcome-email",77 retry: {78 maxAttempts: 5,79 factor: 1.8,80 minTimeoutInMs: 500,81 maxTimeoutInMs: 30_000,82 },83 run: async (payload: { email: string; name: string }) => {84 const { data, error } = await resend.emails.send({85 from: "hello@spinny.dev",86 to: payload.email,87 subject: `Welcome, ${payload.name}`,88 html: `<p>Glad you are here, ${payload.name}.</p>`,89 });9091 if (error) throw error;92 return { messageId: data?.id };93 },94});95```9697سه نکته که باید توجه کنید:98991. **بدون timeout در بدنه run.** پلتفرم زمان اجرا را از طریق `maxDuration` در config مدیریت میکند، نه در runtime.1002. **Throwsها retries هستند.** SDK exceptionها را میگیرد و طبق سیاست `retry` با backoff نمایی دوباره اجرا میکند.1013. **مقدار بازگشتی حفظ میشود.** سایر tasks و frontend شما میتوانند `run.output` را از هر جایی بخوانند.102103## Triggering Tasks104105شما task را از backend خود، routeهای API یا task دیگر فراخوانی میکنید.106107```typescript108import { sendWelcomeEmail } from "@/trigger/send-welcome-email";109110const handle = await sendWelcomeEmail.trigger(111 { email: "user@example.com", name: "Alex" },112 {113 idempotencyKey: `welcome-${userId}`,114 concurrencyKey: `tenant-${tenantId}`,115 queue: { name: "emails", concurrencyLimit: 50 },116 delay: "30s",117 ttl: "10m",118 }119);120121console.log(handle.id); // run_xyz - برای ردیابی یا نمایش پیشرفت استفاده کنید122```123124گزینهها رفتار زیادی را در یک فراخوانی باز میکنند:125126- **`idempotencyKey`** - اگر run با همان کلید قبلاً وجود داشته باشد، SDK handle موجود را بازمیگرداند به جای تکرار کار.127- **`concurrencyKey`** - runs که کلید را به اشتراک میگذارند را سریالی میکند تا از rate limit per-tenant فراتر نروید.128- **`queue.concurrencyLimit`** - cap جهانی برای queue در همه کلیدها.129- **`delay`** - run را برای زمان آینده برنامهریزی میکند.130- **`ttl`** - اگر run تا آن زمان شروع نشده باشد، به طور خودکار منقضی میشود.131132### Batch trigger133134برای workloadهای fan-out، `batchTrigger` تا 500 آیتم در هر فراخوانی میپذیرد و یک run در هر آیتم ایجاد میکند.135136```typescript137await sendWelcomeEmail.batchTrigger(138 newUsers.map((u) => ({139 payload: { email: u.email, name: u.name },140 options: { idempotencyKey: `welcome-${u.id}` },141 }))142);143```144145## Tasks زمانبندی شده146147Cron jobها به اعلانهای کلاس اول تبدیل میشوند. خود schedule یک شیء جداگانه است که میتوانید چندین بار به یک task متصل کنید.148149```typescript150// trigger/daily-digest.ts151import { schedules } from "@trigger.dev/sdk";152153export const dailyDigest = schedules.task({154 id: "daily-digest",155 cron: "0 9 * * *",156 run: async (payload) => {157 console.log("Scheduled at:", payload.timestamp);158 console.log("Last run:", payload.lastTimestamp);159 console.log("Timezone:", payload.timezone);160 console.log("Next 5 runs:", payload.upcoming);161162 await sendDigestForDate(payload.timestamp);163 },164});165```166167برای scheduleهای per-tenant - بگوییم یک cron در هر مشتری - آنها را به صورت پویا از طریق management API ایجاد میکنید.168169```typescript170import { schedules } from "@trigger.dev/sdk";171172await schedules.create({173 task: "daily-digest",174 cron: "0 9 * * *",175 timezone: "America/New_York",176 externalId: `customer_${customerId}`,177 deduplicationKey: `digest-${customerId}`,178});179```180181`deduplicationKey` فراخوانی را idempotent میکند: اجرای مجدد همان کد در زمان deploy scheduleهای تکراری را روی هم انباشته نمیکند.182183## Queues, Concurrency و Idempotency184185سه primitive اکثر نیازهای rate-limiting و ordering را پوشش میدهند.186187```mermaid188graph TB189 Trigger[trigger payload] --> IK{idempotencyKey<br/>دیده شده؟}190 IK -->|بله| Reuse[بازگشت run موجود]191 IK -->|خیر| CK[bucket concurrencyKey]192 CK --> Q[Queue با<br/>concurrencyLimit]193 Q -->|slot در دسترس| Run[اجرای task]194 Q -->|slots پر| Wait[انتظار در queue]195```196197یک الگوی رایج: یک queue به ازای هر tenant با concurrency کوچک per-key برای رعایت rate limit یک vendor، به علاوه یک کلید idempotency برای ایمن کردن retries.198199```typescript200await syncShopifyOrders.trigger(201 { shopId },202 {203 queue: { name: `shopify-${shopId}`, concurrencyLimit: 2 },204 concurrencyKey: shopId,205 idempotencyKey: `sync-${shopId}-${Date.now() / 60_000 | 0}`,206 }207);208```209210## Waits و کار طولانیمدت211212Tasks میتوانند بدون نگه داشتن اتصال یا سوزاندن compute متوقف شوند. پلتفرم state را حفظ میکند و وقتی wait کامل شد تابع را از سر میگیرد.213214```typescript215import { wait } from "@trigger.dev/sdk";216217export const onboarding = task({218 id: "onboarding",219 run: async (payload: { userId: string }) => {220 await sendWelcomeEmail.triggerAndWait({ userId: payload.userId });221 await wait.for({ days: 1 });222 await sendTipsEmail.trigger({ userId: payload.userId });223 await wait.until({ date: oneWeekFromSignup(payload.userId) });224 await sendUpgradeOffer.trigger({ userId: payload.userId });225 },226});227```228229`triggerAndWait` ویژگی کشنده است: یک child task را trigger میکند و parent را تا زمانی که child تمام شود معلق میکند. tasks را مانند توابع async مینویسید، اما orchestration به طور پایدار در طول روزها یا هفتهها اجرا میشود.230231### Human-in-the-loop با `wait.forToken`232233برای جریانهای تأیید و gateهای AI، `wait.forToken` متوقف میشود تا برنامه شما با یک نتیجه callback کند.234235```typescript236import { task, wait } from "@trigger.dev/sdk";237238export const publishPost = task({239 id: "publish-post",240 run: async (payload: { draftId: string }) => {241 const draft = await generateAIContent(payload.draftId);242243 const token = await wait.createToken({ timeout: "7d" });244 await notifyEditor({ draftId: draft.id, token: token.id });245246 const decision = await wait.forToken<{ approved: boolean; notes?: string }>(247 token.id248 );249250 if (decision.approved) {251 return await publish(draft);252 }253 return await applyFeedback(draft, decision.notes);254 },255});256```257258ویرایشگر یک UI باز میکند، پیشنویس را بررسی میکند، روی Approve کلیک میکند و backend شما token را تکمیل میکند. task از جایی که متوقف شد ادامه میدهد - حتی اگر ساعتها یا روزها گذشته باشد.259260## Lifecycle Hooks261262میتوانید `init`, `onStart`, `onSuccess` و `onFailure` را به یک task یا به طور سراسری در `trigger.config.ts` متصل کنید. از آنها برای tracing, error reporting و setup مشترک استفاده کنید.263264```typescript265// trigger.config.ts266export default defineConfig({267 // ...268 init: async () => {269 Sentry.init({ dsn: process.env.SENTRY_DSN });270 },271 onFailure: async ({ error, ctx }) => {272 Sentry.captureException(error, {273 tags: { taskId: ctx.task.id, runId: ctx.run.id },274 });275 },276});277```278279`init` یک بار در هر worker container در boot اجرا میشود، نه در هر run، بنابراین جای مناسبی برای تنظیم clients و poolها است.280281## Realtime در Frontend282283Trigger.dev تغییرات وضعیت run - status, metadata, output - را از طریق streaming API منتشر میکند. React hooks در آن stream مشترک میشوند و به طور خودکار re-render میکنند.284285```typescript286// trigger/process-video.ts287import { task, metadata } from "@trigger.dev/sdk";288289export const processVideo = task({290 id: "process-video",291 run: async (payload: { videoId: string }) => {292 metadata.set("stage", "transcoding");293 await transcode(payload.videoId);294295 metadata.set("stage", "thumbnails");296 await generateThumbnails(payload.videoId);297298 metadata.set("stage", "uploading");299 const url = await uploadToCDN(payload.videoId);300301 return { url };302 },303});304```305306```tsx307// components/VideoStatus.tsx308"use client";309import { useRealtimeRun } from "@trigger.dev/react-hooks";310import type { processVideo } from "@/trigger/process-video";311312export function VideoStatus({313 runId,314 publicAccessToken,315}: {316 runId: string;317 publicAccessToken: string;318}) {319 const { run, error } = useRealtimeRun<typeof processVideo>(runId, {320 accessToken: publicAccessToken,321 });322323 if (error) return <p>Error: {error.message}</p>;324 if (!run) return <p>Loading...</p>;325326 return (327 <div>328 <p>Status: {run.status}</p>329 <p>Stage: {String(run.metadata?.stage ?? "queued")}</p>330 {run.output?.url && <video src={run.output.url} controls />}331 </div>332 );333}334```335336شما public access token را در سمت سرور تولید میکنید، scoped به یک run خاص، و آن را به client ارسال میکنید. hook auth، اتصال مجدد و بهروزرسانیهای افزایشی را مدیریت میکند.337338برای trigger-and-subscribe در یک قدم:339340```tsx341import { useRealtimeTaskTrigger } from "@trigger.dev/react-hooks";342343const { submit, run, isLoading } = useRealtimeTaskTrigger<typeof processVideo>(344 "process-video",345 { accessToken: publicAccessToken }346);347348<button onClick={() => submit({ videoId })} disabled={isLoading}>349 Process video350</button>;351```352353## AI Agents و Streaming354355Trigger.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های طولانیمدت جان سالم به در میبرد.356357```typescript358import { task, metadata } from "@trigger.dev/sdk";359import { streamText } from "ai";360import { anthropic } from "@ai-sdk/anthropic";361362export const researchAgent = task({363 id: "research-agent",364 maxDuration: 1800,365 run: async (payload: { question: string }) => {366 const result = streamText({367 model: anthropic("claude-opus-4-7"),368 system: "You are a research assistant. Use the web.",369 prompt: payload.question,370 tools: { webSearch },371 });372373 let fullText = "";374 for await (const chunk of result.textStream) {375 fullText += chunk;376 metadata.set("partial", fullText);377 }378379 return { answer: fullText, usage: await result.usage };380 },381});382```383384frontend از `useRealtimeRun` استفاده میکند و `run.metadata.partial` را برای render پاسخ streaming میخواند، به همان روشی که یک chat completion را render میکردید - با این تفاوت که این یکی از reload کامل صفحه جان سالم به در میبرد.385386## Deploying387388Deployها tasks شما را به یک bundle ورژندار کامپایل میکنند، یک container میسازند و ترافیک را به صورت اتمیک تعویض میکنند. runs قدیمی in-flight به استفاده از نسخه قبلی ادامه میدهند.389390```bash391npx trigger.dev@latest deploy --env prod392```393394در CI معمولاً این را به همان workflow متصل میکنید که app شما را ارسال میکند:395396```yaml397# .github/workflows/deploy.yml398- name: Deploy Trigger.dev399 env:400 TRIGGER_ACCESS_TOKEN: ${{ secrets.TRIGGER_ACCESS_TOKEN }}401 run: npx trigger.dev@latest deploy --env prod402```403404برای محیطهای preview، `--env preview --branch ${{ github.head_ref }}` را عبور دهید و Trigger.dev یک محیط ایزوله در هر branch ایجاد میکند، که نحوه برخورد Vercel با preview deploymentها را منعکس میکند.405406## Self-Hosting در مقابل Cloud407408Trigger.dev تحت لایسنس Apache 2.0 متنباز است. میتوانید روی هر پلتفرم container (Docker Compose, Kubernetes, Fly.io) self-host کنید یا از cloud مدیریتشده در trigger.dev استفاده کنید.409410| جنبه | Cloud | Self-hosted |411|--------|-------|-------------|412| **Setup** | ثبتنام، اجرای `init` | اجرای docker-compose یا Helm chart |413| **Scaling** | خودکار | مسئولیت شما |414| **Pricing** | به ازای run + به ازای compute | فقط هزینه infra |415| **Compliance** | SOC 2 | هرچه محیط شما فراهم میکند |416| **بهترین برای** | اکثر تیمها | residency داده سختگیرانه، infra سفارشی |417418SDK و CLI بین حالتها یکسان هستند - یک profile flag را تغییر میدهید و به instance خود اشاره میکنید.419420## Best Practices421422### 1. payloads را کوچک و قابل serialize نگه دارید423424IDها و referenceها را عبور دهید، نه اشیاء کامل. دادهها را در داخل task بگیرید. این queue را کوچک نگه میدارد، payloads را برای log کردن ارزان میکند و به شما اجازه میدهد منبع داده را بدون trigger مجدد تغییر دهید.425426### 2. کلیدهای Idempotency در هر فراخوانی خارجی427428`idempotencyKey` در trigger task را با کلیدهای idempotency در APIهای vendor خود (Stripe, OpenAI, و غیره) ترکیب کنید. retriesها end-to-end ایمن خواهند بود.429430### 3. از `triggerAndWait` برای orchestration استفاده کنید، نه `Promise.all` از triggerها431432یک parent که `triggerAndWait` را فراخوانی میکند به طور پایدار child tasks را compose میکند. یک parent که trigger میکند و فوراً resolve میشود، observability زنجیره را از دست میدهد.433434### 4. runs را tag کنید435436به triggerها `tags` اضافه کنید (`tags: ["user:123", "feature:onboarding"]`) تا بتوانید dashboard و management API را بر اساس ابعاد business فیلتر کنید.437438### 5. `init` را idempotent نگه دارید439440در هر cold start اجرا میشود. از migrations یا اثرات جانبی one-shot در آنجا اجتناب کنید.441442## نتیجهگیری443444Trigger.dev دستههای کاری را که قبلاً برای ساختن یک سیستم job از صفر مورد نیاز بودند، حذف میکند. async TypeScript مینویسید، آن را از هر جایی فراخوانی میکنید و پلتفرم به شما اجرای پایدار، scheduling، queues، retries، بهروزرسانیهای realtime و الگوهای human-in-the-loop را به صورت out of the box میدهد.445446همان سطحی که یک cron شبانه را قدرت میدهد، سطحی است که یک AI agent چند مرحلهای را قدرت میدهد که به frontend stream میکند و برای review متوقف میشود. این همگرایی است که framework را در 2026 شایسته یک نگاه جدی میکند، چه یک SaaS را اداره میکنید که به کار پسزمینه قابل اعتماد نیاز دارد، چه ویژگیهای AI را ارسال میکنید که از یک serverless timeout جان سالم به در میبرند.447448> **چکلیست شروع:**449>450> - [x] در trigger.dev ثبتنام کنید یا stack Docker self-hosted را اجرا کنید451> - [x] `npx trigger.dev@latest init` در پروژه شما452> - [x] اولین task خود را با `task({ id, run })` تعریف کنید453> - [x] آن را از API خود trigger کنید و run را در dashboard ببینید454> - [x] برای امنیت production `idempotencyKey` و `concurrencyKey` اضافه کنید455> - [x] `useRealtimeRun` را به یک status component متصل کنید456> - [x] با `trigger.dev deploy --env prod` از CI deploy کنید457
:Trigger.dev: کارهای پسزمینه پایدار و گردشکارهای AI در TypeScriptlines 1-457 (END) — press q to close