spinny:~/writing $ vim trigger-dev-background-jobs-guide.md
1~2اکثر برنامههای production به کاری نیاز دارند که در چرخه request/response جا نمیگیرد: ارسال ایمیل، پردازش آپلودها، اجرای pipelineهای AI، همگامسازی دادههای شخص ثالث، تولید گزارش. پاسخ سنتی یک صف (Redis, SQS, RabbitMQ)، یک ناوگان worker، یک scheduler و یک پشته شکننده از کد چسب است که در هر deploy میشکند.3~4[Trigger.dev](https://trigger.dev) آن stack را در یک TypeScript SDK واحد فشرده میکند. شما توابع را مینویسید، آنها را از هر جایی فراخوانی میکنید و پلتفرم queueing، retries، observability، scheduling و اجرای پایدار را مدیریت میکند. Tasks تا زمانی که نیاز است اجرا میشوند - بدون timeout serverless 10 ثانیهای، بدون کار از دست رفته در redeploy.5~6## چرا Trigger.dev7~8تغییر در 2026 اجرای پایدار است. Workflows باید از restart، crash، deploy و rate limit جان سالم به در ببرند. آنها همچنین باید پیشرفت را به صورت realtime به UI استریم کنند و برای ورودی انسانی متوقف شوند. Trigger.dev در نسخه 3 حول این الزامات بازسازی شد و به گسترش سطح زیرساخت AI خود ادامه میدهد.9~10```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```20~21مدل ساده است: tasks را به عنوان exports تعریف میکنید، SDK آنها را برمیدارد، پلتفرم آنها را برنامهریزی و در کانتینرهای ایزوله اجرا میکند و وضعیت run حفظ میشود تا بتوانید ادامه دهید، دوباره تلاش کنید و مشاهده کنید.22~23## شروع24~25### یک پروژه را مقداردهی اولیه کنید26~27```bash28npx trigger.dev@latest login29npx trigger.dev@latest init30```31~32این یک فایل `trigger.config.ts` و یک دایرکتوری `trigger/` با tasks نمونه ایجاد میکند. فایل config منبع حقیقت برای پروژه شماست: کدام دایرکتوریها حاوی tasks هستند، تنظیمات build، lifecycle hooks و گزینههای runtime.33~34```typescript35// trigger.config.ts36import { defineConfig } from "@trigger.dev/sdk";37~38export 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```55~56### tasks را به صورت محلی اجرا کنید57~58```bash59npx trigger.dev@latest dev60```61~62سرور dev به cloud متصل میشود، tasks شما را ثبت میکند و runs را از طریق کد محلی شما استریم میکند. در ویرایشگر خود breakpoint میگذارید و آنها را روی triggerهای واقعی میزنید - همان loop که در هر پروژه عادی Node.js استفاده میکنید.63~64## تعریف یک Task65~66Task یک شیء است که با `id` منحصر به فرد و یک تابع `run` صادر میشود. SDK exports را در `dirs` بررسی میکند و آنها را به طور خودکار ثبت میکند.67~68```typescript69// trigger/send-welcome-email.ts70import { task } from "@trigger.dev/sdk";71import { Resend } from "resend";72~73const resend = new Resend(process.env.RESEND_API_KEY);74~75export 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 });90~91 if (error) throw error;92 return { messageId: data?.id };93 },94});95```96~97سه نکته که باید توجه کنید:98~991. **بدون timeout در بدنه run.** پلتفرم زمان اجرا را از طریق `maxDuration` در config مدیریت میکند، نه در runtime.1002. **Throwsها retries هستند.** SDK exceptionها را میگیرد و طبق سیاست `retry` با backoff نمایی دوباره اجرا میکند.1013. **مقدار بازگشتی حفظ میشود.** سایر tasks و frontend شما میتوانند `run.output` را از هر جایی بخوانند.102~103## Triggering Tasks104~105شما task را از backend خود، routeهای API یا task دیگر فراخوانی میکنید.106~107```typescript108import { sendWelcomeEmail } from "@/trigger/send-welcome-email";109~110const 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);120~121console.log(handle.id); // run_xyz - برای ردیابی یا نمایش پیشرفت استفاده کنید122```123~124گزینهها رفتار زیادی را در یک فراخوانی باز میکنند:125~126- **`idempotencyKey`** - اگر run با همان کلید قبلاً وجود داشته باشد، SDK handle موجود را بازمیگرداند به جای تکرار کار.127- **`concurrencyKey`** - runs که کلید را به اشتراک میگذارند را سریالی میکند تا از rate limit per-tenant فراتر نروید.128- **`queue.concurrencyLimit`** - cap جهانی برای queue در همه کلیدها.129- **`delay`** - run را برای زمان آینده برنامهریزی میکند.130- **`ttl`** - اگر run تا آن زمان شروع نشده باشد، به طور خودکار منقضی میشود.131~132### Batch trigger133~134برای workloadهای fan-out، `batchTrigger` تا 500 آیتم در هر فراخوانی میپذیرد و یک run در هر آیتم ایجاد میکند.135~136```typescript137await sendWelcomeEmail.batchTrigger(138 newUsers.map((u) => ({139 payload: { email: u.email, name: u.name },140 options: { idempotencyKey: `welcome-${u.id}` },141 }))142);143```144~145## Tasks زمانبندی شده146~147Cron jobها به اعلانهای کلاس اول تبدیل میشوند. خود schedule یک شیء جداگانه است که میتوانید چندین بار به یک task متصل کنید.148~149```typescript150// trigger/daily-digest.ts151import { schedules } from "@trigger.dev/sdk";152~153export 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);161~162 await sendDigestForDate(payload.timestamp);163 },164});165```166~167برای scheduleهای per-tenant - بگوییم یک cron در هر مشتری - آنها را به صورت پویا از طریق management API ایجاد میکنید.168~169```typescript170import { schedules } from "@trigger.dev/sdk";171~172await 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```180~181`deduplicationKey` فراخوانی را idempotent میکند: اجرای مجدد همان کد در زمان deploy scheduleهای تکراری را روی هم انباشته نمیکند.182~183## Queues, Concurrency و Idempotency184~185سه primitive اکثر نیازهای rate-limiting و ordering را پوشش میدهند.186~187```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```196~197یک الگوی رایج: یک queue به ازای هر tenant با concurrency کوچک per-key برای رعایت rate limit یک vendor، به علاوه یک کلید idempotency برای ایمن کردن retries.198~199```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```209~210## Waits و کار طولانیمدت211~212Tasks میتوانند بدون نگه داشتن اتصال یا سوزاندن compute متوقف شوند. پلتفرم state را حفظ میکند و وقتی wait کامل شد تابع را از سر میگیرد.213~214```typescript215import { wait } from "@trigger.dev/sdk";216~217export 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```228~229`triggerAndWait` ویژگی کشنده است: یک child task را trigger میکند و parent را تا زمانی که child تمام شود معلق میکند. tasks را مانند توابع async مینویسید، اما orchestration به طور پایدار در طول روزها یا هفتهها اجرا میشود.230~231### Human-in-the-loop با `wait.forToken`232~233برای جریانهای تأیید و gateهای AI، `wait.forToken` متوقف میشود تا برنامه شما با یک نتیجه callback کند.234~235```typescript236import { task, wait } from "@trigger.dev/sdk";237~238export const publishPost = task({239 id: "publish-post",240 run: async (payload: { draftId: string }) => {241 const draft = await generateAIContent(payload.draftId);242~243 const token = await wait.createToken({ timeout: "7d" });244 await notifyEditor({ draftId: draft.id, token: token.id });245~246 const decision = await wait.forToken<{ approved: boolean; notes?: string }>(247 token.id248 );249~250 if (decision.approved) {251 return await publish(draft);252 }253 return await applyFeedback(draft, decision.notes);254 },255});256```257~258ویرایشگر یک UI باز میکند، پیشنویس را بررسی میکند، روی Approve کلیک میکند و backend شما token را تکمیل میکند. task از جایی که متوقف شد ادامه میدهد - حتی اگر ساعتها یا روزها گذشته باشد.259~260## Lifecycle Hooks261~262میتوانید `init`, `onStart`, `onSuccess` و `onFailure` را به یک task یا به طور سراسری در `trigger.config.ts` متصل کنید. از آنها برای tracing, error reporting و setup مشترک استفاده کنید.263~264```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```278~279`init` یک بار در هر worker container در boot اجرا میشود، نه در هر run، بنابراین جای مناسبی برای تنظیم clients و poolها است.280~281## Realtime در Frontend282~283Trigger.dev تغییرات وضعیت run - status, metadata, output - را از طریق streaming API منتشر میکند. React hooks در آن stream مشترک میشوند و به طور خودکار re-render میکنند.284~285```typescript286// trigger/process-video.ts287import { task, metadata } from "@trigger.dev/sdk";288~289export const processVideo = task({290 id: "process-video",291 run: async (payload: { videoId: string }) => {292 metadata.set("stage", "transcoding");293 await transcode(payload.videoId);294~295 metadata.set("stage", "thumbnails");296 await generateThumbnails(payload.videoId);297~298 metadata.set("stage", "uploading");299 const url = await uploadToCDN(payload.videoId);300~301 return { url };302 },303});304```305~306```tsx307// components/VideoStatus.tsx308"use client";309import { useRealtimeRun } from "@trigger.dev/react-hooks";310import type { processVideo } from "@/trigger/process-video";311~312export 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 });322~323 if (error) return <p>Error: {error.message}</p>;324 if (!run) return <p>Loading...</p>;325~326 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```335~336شما public access token را در سمت سرور تولید میکنید، scoped به یک run خاص، و آن را به client ارسال میکنید. hook auth، اتصال مجدد و بهروزرسانیهای افزایشی را مدیریت میکند.337~338برای trigger-and-subscribe در یک قدم:339~340```tsx341import { useRealtimeTaskTrigger } from "@trigger.dev/react-hooks";342~343const { submit, run, isLoading } = useRealtimeTaskTrigger<typeof processVideo>(344 "process-video",345 { accessToken: publicAccessToken }346);347~348<button onClick={() => submit({ videoId })} disabled={isLoading}>349 Process video350</button>;351```352~353## AI Agents و Streaming354~355Trigger.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های طولانیمدت جان سالم به در میبرد.356~357```typescript358import { task, metadata } from "@trigger.dev/sdk";359import { streamText } from "ai";360import { anthropic } from "@ai-sdk/anthropic";361~362export 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 });372~373 let fullText = "";374 for await (const chunk of result.textStream) {375 fullText += chunk;376 metadata.set("partial", fullText);377 }378~379 return { answer: fullText, usage: await result.usage };380 },381});382```383~384frontend از `useRealtimeRun` استفاده میکند و `run.metadata.partial` را برای render پاسخ streaming میخواند، به همان روشی که یک chat completion را render میکردید - با این تفاوت که این یکی از reload کامل صفحه جان سالم به در میبرد.385~386## Deploying387~388Deployها tasks شما را به یک bundle ورژندار کامپایل میکنند، یک container میسازند و ترافیک را به صورت اتمیک تعویض میکنند. runs قدیمی in-flight به استفاده از نسخه قبلی ادامه میدهند.389~390```bash391npx trigger.dev@latest deploy --env prod392```393~394در CI معمولاً این را به همان workflow متصل میکنید که app شما را ارسال میکند:395~396```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```403~404برای محیطهای preview، `--env preview --branch ${{ github.head_ref }}` را عبور دهید و Trigger.dev یک محیط ایزوله در هر branch ایجاد میکند، که نحوه برخورد Vercel با preview deploymentها را منعکس میکند.405~406## Self-Hosting در مقابل Cloud407~408Trigger.dev تحت لایسنس Apache 2.0 متنباز است. میتوانید روی هر پلتفرم container (Docker Compose, Kubernetes, Fly.io) self-host کنید یا از cloud مدیریتشده در trigger.dev استفاده کنید.409~410| جنبه | 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 سفارشی |417~418SDK و CLI بین حالتها یکسان هستند - یک profile flag را تغییر میدهید و به instance خود اشاره میکنید.419~420## Best Practices421~422### 1. payloads را کوچک و قابل serialize نگه دارید423~424IDها و referenceها را عبور دهید، نه اشیاء کامل. دادهها را در داخل task بگیرید. این queue را کوچک نگه میدارد، payloads را برای log کردن ارزان میکند و به شما اجازه میدهد منبع داده را بدون trigger مجدد تغییر دهید.425~426### 2. کلیدهای Idempotency در هر فراخوانی خارجی427~428`idempotencyKey` در trigger task را با کلیدهای idempotency در APIهای vendor خود (Stripe, OpenAI, و غیره) ترکیب کنید. retriesها end-to-end ایمن خواهند بود.429~430### 3. از `triggerAndWait` برای orchestration استفاده کنید، نه `Promise.all` از triggerها431~432یک parent که `triggerAndWait` را فراخوانی میکند به طور پایدار child tasks را compose میکند. یک parent که trigger میکند و فوراً resolve میشود، observability زنجیره را از دست میدهد.433~434### 4. runs را tag کنید435~436به triggerها `tags` اضافه کنید (`tags: ["user:123", "feature:onboarding"]`) تا بتوانید dashboard و management API را بر اساس ابعاد business فیلتر کنید.437~438### 5. `init` را idempotent نگه دارید439~440در هر cold start اجرا میشود. از migrations یا اثرات جانبی one-shot در آنجا اجتناب کنید.441~442## نتیجهگیری443~444Trigger.dev دستههای کاری را که قبلاً برای ساختن یک سیستم job از صفر مورد نیاز بودند، حذف میکند. async TypeScript مینویسید، آن را از هر جایی فراخوانی میکنید و پلتفرم به شما اجرای پایدار، scheduling، queues، retries، بهروزرسانیهای realtime و الگوهای human-in-the-loop را به صورت out of the box میدهد.445~446همان سطحی که یک cron شبانه را قدرت میدهد، سطحی است که یک AI agent چند مرحلهای را قدرت میدهد که به frontend stream میکند و برای review متوقف میشود. این همگرایی است که framework را در 2026 شایسته یک نگاه جدی میکند، چه یک SaaS را اداره میکنید که به کار پسزمینه قابل اعتماد نیاز دارد، چه ویژگیهای AI را ارسال میکنید که از یک serverless timeout جان سالم به در میبرند.447~448> **چکلیست شروع:**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~
NORMAL · trigger-dev-background-jobs-guide.md [readonly]457 lines · :q to close