spinny:~/writing $ less trigger-dev-background-jobs-guide.md
12แอปพลิเคชัน production ส่วนใหญ่ต้องการงานที่ไม่เข้ากับวงจร request/response: การส่งอีเมล การประมวลผลการอัปโหลด การรัน AI pipelines การซิงค์ข้อมูลจากบุคคลที่สาม การสร้างรายงาน คำตอบดั้งเดิมคือคิว (Redis, SQS, RabbitMQ), worker fleet, scheduler และกองโค้ด glue ที่เปราะบางซึ่งจะแตกในทุกๆ การ deploy34[Trigger.dev](https://trigger.dev) ยุบ stack นั้นให้เป็น TypeScript SDK เดียว คุณเขียน functions เรียกใช้จากที่ไหนก็ได้ และแพลตฟอร์มจัดการ queueing, retries, observability, scheduling และการประมวลผลที่ทนทาน Tasks ทำงานนานเท่าที่ต้องการ - ไม่มี timeout serverless 10 วินาที ไม่มีงานที่หายไปเมื่อ redeploy56## ทำไมต้อง Trigger.dev78การเปลี่ยนแปลงในปี 2026 คือการประมวลผลที่ทนทาน Workflows ต้องอยู่รอดจากการรีสตาร์ท การ crash การ deploy และ rate limit พวกเขาต้อง stream ความคืบหน้าไปยัง 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[Realtime Stream]17 Realtime --> UI[React UI]18 Worker --> Storage[Run State Store]19```2021โมเดลนั้นเรียบง่าย: คุณกำหนด tasks เป็น exports, SDK รับมันมา แพลตฟอร์มจัดตารางและรันใน containers ที่แยกออกจากกันและสถานะของ run จะถูกเก็บไว้เพื่อให้คุณสามารถดำเนินการต่อ ลองใหม่และสังเกตได้2223## เริ่มต้น2425### เริ่มต้นโปรเจค2627```bash28npx trigger.dev@latest login29npx trigger.dev@latest init30```3132สิ่งนี้สร้างไฟล์ `trigger.config.ts` และไดเร็กทอรี `trigger/` พร้อม tasks ตัวอย่าง ไฟล์ config คือแหล่งความจริงสำหรับโปรเจคของคุณ: ไดเร็กทอรีใดมี tasks การตั้งค่า build, lifecycle hooks และตัวเลือก runtime3334```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 เชื่อมต่อกับคลาวด์ ลงทะเบียน tasks ของคุณและ stream runs ผ่านโค้ดในเครื่องของคุณ คุณตั้งค่า breakpoints ใน editor ของคุณและตีถึงพวกเขาบน trigger จริง - วงรอบเดียวกันที่คุณจะใช้ในโปรเจค Node.js ปกติ6364## การกำหนด Task6566Task คือ object ที่ส่งออกพร้อม `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 ไม่ใช่ runtime1002. **Throws คือ retries.** SDK จับ exceptions และรันใหม่ด้วย exponential backoff ตามนโยบาย `retry`1013. **ค่าที่ส่งคืนถูกเก็บไว้** Tasks อื่นและ frontend ของคุณสามารถอ่าน `run.output` จากที่ไหนก็ได้102103## การ Trigger 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 ที่มี key เดียวกันมีอยู่แล้ว SDK จะส่งคืน handle ที่มีอยู่แทนการทำงานซ้ำ127- **`concurrencyKey`** - ทำการอนุกรม runs ที่แชร์ key ดังนั้นคุณจะไม่เกิน rate limit ต่อ tenant128- **`queue.concurrencyLimit`** - ขอบเขตทั่วโลกสำหรับ queue ในทุก keys129- **`delay`** - กำหนดเวลา run สำหรับเวลาในอนาคต130- **`ttl`** - หาก run ยังไม่ได้เริ่มต้นภายในเวลานั้น ให้หมดอายุโดยอัตโนมัติ131132### Batch trigger133134สำหรับ workload fan-out, `batchTrigger` ยอมรับ items สูงสุด 500 รายการต่อการเรียกและสร้าง run หนึ่งรายการต่อ item135136```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 jobs กลายเป็นการประกาศชั้นแรก Schedule เองเป็น object แยกที่คุณสามารถแนบกับ 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 ต่อ tenant - เช่น cron หนึ่งต่อลูกค้า - คุณสร้างพวกเขาแบบไดนามิกผ่าน management API168169```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 ไม่ทำให้ schedules ซ้ำกัน182183## Queues, Concurrency และ Idempotency184185primitives สามตัวครอบคลุมความต้องการ 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 ต่อ key เล็กเพื่อเคารพ rate limit ของ vendor บวกกับ idempotency key เพื่อให้ 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 แพลตฟอร์มเก็บสถานะและดำเนินการฟังก์ชันต่อเมื่อ 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` คือฟีเจอร์เด็ดขาด: มัน trigger task ลูกและระงับ parent จนกว่า child จะเสร็จสมบูรณ์ คุณรวบรวม tasks เหมือน async functions แต่การประสานทำงานอย่างทนทานข้ามวันหรือสัปดาห์230231### Human-in-the-loop ด้วย `wait.forToken`232233สำหรับขั้นตอนการอนุมัติและ AI gates, `wait.forToken` หยุดชั่วคราวจนกว่าแอปพลิเคชันของคุณจะเรียกกลับด้วยผลลัพธ์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```257258editor เปิด UI ตรวจสอบร่าง คลิกอนุมัติและ 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 และ pools280281## 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 ฝั่งเซิร์ฟเวอร์ที่กำหนดขอบเขตให้กับ 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, real-time metadata, human-in-the-loop - คือสิ่งที่ agents ต้องการ คุณ stream tokens จาก model provider เข้าสู่ `metadata` ในขณะที่ run กำลังเกิดขึ้น frontend แสดงผลแบบสด และ run อยู่รอดผ่าน tool calls ที่ยาวนานโดยไม่เผา timeout serverless356357```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` เพื่อแสดง response แบบ streaming ในวิธีเดียวกับที่คุณจะแสดง chat completion - ยกเว้นว่าอันนี้อยู่รอดจาก reload หน้าเต็ม385386## Deploying387388Deploys คอมไพล์ tasks ของคุณเป็น bundle ที่มีเวอร์ชัน สร้าง container และสลับการรับส่งข้อมูลแบบ atomic Old in-flight runs ยังคงใช้เวอร์ชันก่อนหน้า389390```bash391npx trigger.dev@latest deploy --env prod392```393394ใน CI โดยปกติคุณเชื่อมต่อสิ่งนี้กับ workflow เดียวกันที่จัดส่งแอปของคุณ: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สำหรับ environment preview ส่ง `--env preview --branch ${{ github.head_ref }}` และ Trigger.dev สร้างสภาพแวดล้อมที่แยกต่อ branch สะท้อนวิธีที่ Vercel จัดการ preview deployments405406## Self-Hosting vs Cloud407408Trigger.dev เป็นโอเพ่นซอร์สภายใต้ใบอนุญาต Apache 2.0 คุณสามารถ self-host บนแพลตฟอร์ม container ใดๆ (Docker Compose, Kubernetes, Fly.io) หรือใช้ cloud ที่จัดการที่ trigger.dev409410| ด้าน | Cloud | Self-hosted |411|--------|-------|-------------|412| **Setup** | ลงทะเบียน รัน `init` | รัน docker-compose หรือ Helm chart |413| **Scaling** | อัตโนมัติ | ความรับผิดชอบของคุณ |414| **Pricing** | ต่อ run + ต่อ compute | ค่า infra เท่านั้น |415| **Compliance** | SOC 2 | สิ่งที่สภาพแวดล้อมของคุณให้ |416| **เหมาะสำหรับ** | ทีมส่วนใหญ่ | data residency เข้มงวด, infra ที่กำหนดเอง |417418SDK และ CLI เหมือนกันระหว่างโหมด - คุณเปลี่ยน profile flag และชี้ไปที่ instance ของคุณเอง419420## Best Practices421422### 1. รักษา payloads ให้เล็กและ serializable ได้423424ส่ง IDs และการอ้างอิง ไม่ใช่ objects เต็ม ดึงข้อมูลภายใน task สิ่งนี้ช่วยให้ queue เล็ก payloads ถูกในการ log และให้คุณเปลี่ยนแหล่งข้อมูลโดยไม่ต้อง re-trigger425426### 2. Idempotency keys ในทุกการเรียกภายนอก427428รวม `idempotencyKey` บน trigger ของ task กับ idempotency keys บน vendor APIs (Stripe, OpenAI, ฯลฯ) Retries จะปลอดภัย end-to-end429430### 3. ใช้ `triggerAndWait` สำหรับการประสาน ไม่ใช่ `Promise.all` ของ triggers431432parent ที่เรียก `triggerAndWait` รวบรวม tasks ลูกอย่างทนทาน parent ที่ trigger และแก้ไขทันทีจะสูญเสียความสามารถในการสังเกตของ chain433434### 4. Tag runs435436เพิ่ม `tags` ให้กับ triggers (`tags: ["user:123", "feature:onboarding"]`) เพื่อให้คุณสามารถกรอง dashboard และ management API ตามมิติทางธุรกิจ437438### 5. รักษา `init` ให้ idempotent439440มันรันทุก cold start หลีกเลี่ยง migrations หรือ side effects แบบ one-shot ที่นั่น441442## บทสรุป443444Trigger.dev ลบประเภทของงานที่เคยต้องสร้างระบบ job จากศูนย์ คุณเขียน async TypeScript เรียกใช้จากที่ไหนก็ได้และแพลตฟอร์มให้คุณการประมวลผลที่ทนทาน การกำหนดเวลา queues, retries, การอัปเดตแบบ real-time และรูปแบบ human-in-the-loop ออกจากกล่อง445446พื้นผิวเดียวกันที่ขับเคลื่อน cron กลางคืนคือพื้นผิวที่ขับเคลื่อน AI agent หลายขั้นตอนที่ stream ไปยัง frontend และหยุดชั่วคราวสำหรับการตรวจสอบ การบรรจบกันนั้นคือสิ่งที่ทำให้ framework ควรค่าแก่การมองอย่างจริงจังในปี 2026 ไม่ว่าคุณจะกำลังจัดการ SaaS ที่ต้องการงาน background ที่เชื่อถือได้หรือจัดส่งฟีเจอร์ AI ที่อยู่รอดเหนือ timeout serverless447448> **Checklist เริ่มต้น:**449>450> - [x] ลงทะเบียนที่ trigger.dev หรือรัน stack Docker self-hosted451> - [x] `npx trigger.dev@latest init` ในโปรเจคของคุณ452> - [x] กำหนด task แรกของคุณด้วย `task({ id, run })`453> - [x] Trigger จาก API ของคุณและดู run ใน dashboard454> - [x] เพิ่ม `idempotencyKey` และ `concurrencyKey` สำหรับความปลอดภัยใน production455> - [x] เชื่อม `useRealtimeRun` เข้ากับ status component456> - [x] Deploy ด้วย `trigger.dev deploy --env prod` จาก CI457
:Trigger.dev: Background Jobs ที่ทนทานและ AI Workflows ใน TypeScriptlines 1-457 (END) — press q to close