spinny:~/writing $ vim trigger-dev-background-jobs-guide.md
1~2Sebagian besar aplikasi production memerlukan pekerjaan yang tidak cocok dengan siklus request/response: mengirim email, memproses unggahan, menjalankan pipeline AI, sinkronisasi data pihak ketiga, menghasilkan laporan. Jawaban tradisional adalah queue (Redis, SQS, RabbitMQ), armada worker, scheduler, dan tumpukan kode glue rapuh yang rusak setiap deploy.3~4[Trigger.dev](https://trigger.dev) merampingkan stack tersebut menjadi satu SDK TypeScript. Anda menulis fungsi, memanggilnya dari mana saja, dan platform menangani queueing, retries, observability, scheduling, dan eksekusi yang tahan lama. Tasks berjalan selama yang dibutuhkan - tanpa timeout serverless 10 detik, tidak ada pekerjaan yang hilang saat redeploy.5~6## Mengapa Trigger.dev7~8Pergeseran di 2026 adalah eksekusi yang tahan lama. Workflow harus bertahan terhadap restart, crash, deploy, dan rate limit. Mereka juga harus melakukan streaming progress ke UI secara real time dan jeda untuk input manusia. Trigger.dev dibangun ulang seputar persyaratan ini dengan versi 3 dan terus memperluas permukaan infrastruktur AI-nya.9~10```mermaid11graph LR12 App[Aplikasi Anda] -->|trigger| API[API Trigger.dev]13 API --> Queue[Queue Tahan Lama]14 Queue --> Worker[Container Worker]15 Worker -->|run task| Task[Kode Task Anda]16 Task -->|metadata| Realtime[Stream Realtime]17 Realtime --> UI[UI React]18 Worker --> Storage[Store Status Run]19```20~21Modelnya sederhana: Anda mendefinisikan tasks sebagai exports, SDK mengambilnya, platform menjadwalkan dan menjalankannya di container terisolasi, dan status run dipertahankan sehingga Anda dapat melanjutkan, mencoba lagi, dan mengamati.22~23## Memulai24~25### Inisialisasi proyek26~27```bash28npx trigger.dev@latest login29npx trigger.dev@latest init30```31~32Ini membuat file `trigger.config.ts` dan direktori `trigger/` dengan tasks contoh. File config adalah sumber kebenaran untuk proyek Anda: direktori mana yang berisi tasks, pengaturan build, lifecycle hooks, dan opsi 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### Jalankan tasks secara lokal57~58```bash59npx trigger.dev@latest dev60```61~62Server dev terhubung ke cloud, mendaftarkan tasks Anda, dan melakukan streaming runs melalui kode lokal Anda. Anda menetapkan breakpoint di editor Anda dan mencapainya pada trigger nyata - loop yang sama yang akan Anda gunakan dalam proyek Node.js normal.63~64## Mendefinisikan Task65~66Task adalah objek yang diekspor dengan `id` unik dan fungsi `run`. SDK memeriksa exports di seluruh `dirs` dan mendaftarkannya secara otomatis.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~97Tiga hal yang perlu diperhatikan:98~991. **Tidak ada timeout di body run.** Platform mengelola waktu eksekusi melalui `maxDuration` di config, bukan di runtime.1002. **Throws adalah retries.** SDK menangkap exception dan menjalankan kembali dengan exponential backoff sesuai kebijakan `retry`.1013. **Nilai pengembalian dipertahankan.** Tasks lain dan frontend Anda dapat membaca `run.output` dari mana saja.102~103## Triggering Tasks104~105Anda memanggil task dari backend, route API, atau task lain.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 - gunakan untuk melacak atau menampilkan progress122```123~124Opsi membuka banyak perilaku dalam satu panggilan:125~126- **`idempotencyKey`** - jika run dengan key yang sama sudah ada, SDK mengembalikan handle yang ada alih-alih duplikat pekerjaan.127- **`concurrencyKey`** - menserialkan runs yang berbagi key sehingga Anda tidak melebihi rate limit per-tenant.128- **`queue.concurrencyLimit`** - cap global untuk queue di seluruh keys.129- **`delay`** - menjadwalkan run untuk waktu di masa depan.130- **`ttl`** - jika run belum dimulai pada saat itu, kadaluarsa secara otomatis.131~132### Batch trigger133~134Untuk workload fan-out, `batchTrigger` menerima hingga 500 item per panggilan dan membuat satu run per item.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 Terjadwal146~147Cron jobs menjadi deklarasi kelas pertama. Schedule itu sendiri adalah objek terpisah yang dapat Anda lampirkan ke task beberapa kali.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~167Untuk schedules per-tenant - katakanlah, satu cron per pelanggan - Anda membuatnya secara dinamis melalui 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` membuat panggilan idempotent: menjalankan kembali kode yang sama saat deploy tidak menumpuk schedules duplikat.182~183## Queues, Concurrency, dan Idempotency184~185Tiga primitif mencakup sebagian besar kebutuhan rate-limiting dan ordering.186~187```mermaid188graph TB189 Trigger[trigger payload] --> IK{idempotencyKey<br/>terlihat?}190 IK -->|ya| Reuse[Kembalikan run yang ada]191 IK -->|tidak| CK[bucket concurrencyKey]192 CK --> Q[Queue dengan<br/>concurrencyLimit]193 Q -->|slot tersedia| Run[Jalankan task]194 Q -->|slot penuh| Wait[Tunggu di queue]195```196~197Pola umum: satu queue per tenant dengan concurrency per-key kecil untuk menghormati rate limit vendor, ditambah idempotency key untuk membuat retries aman.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 dan Pekerjaan Berjalan Lama211~212Tasks dapat dijeda tanpa mempertahankan koneksi atau membakar compute. Platform mempertahankan status dan melanjutkan fungsi ketika wait selesai.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` adalah fitur pembunuh: ini memicu task anak dan menangguhkan parent sampai anak selesai. Anda menyusun tasks seperti fungsi async, tetapi orkestrasi berjalan tahan lama selama berhari-hari atau berminggu-minggu.230~231### Human-in-the-loop dengan `wait.forToken`232~233Untuk approval flow dan AI gates, `wait.forToken` jeda sampai aplikasi Anda callback dengan hasil.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~258Editor membuka UI, meninjau draft, klik Approve, dan backend Anda menyelesaikan token. Task berlanjut dari tempat ia berhenti - bahkan jika sudah berlalu berjam-jam atau berhari-hari.259~260## Lifecycle Hooks261~262Anda dapat melampirkan `init`, `onStart`, `onSuccess`, dan `onFailure` ke task atau secara global di `trigger.config.ts`. Gunakan untuk tracing, error reporting, dan setup bersama.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` berjalan sekali per worker container saat boot, bukan per run, jadi ini adalah tempat yang tepat untuk mengatur clients dan pools.280~281## Realtime di Frontend282~283Trigger.dev mempublikasikan perubahan status run - status, metadata, output - melalui API streaming. React hooks subscribe ke stream itu dan re-render secara otomatis.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~336Anda menghasilkan public access token sisi server, dengan scope ke run tertentu, dan mengirimkannya ke client. Hook menangani auth, reconnection, dan pembaruan inkremental.337~338Untuk trigger-and-subscribe dalam satu langkah: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 dan Streaming354~355Trigger.dev telah menjadi runtime populer untuk agent AI karena primitif yang sama - eksekusi tahan lama, retries, waits, metadata real-time, human-in-the-loop - persis yang dibutuhkan agent. Anda streaming token dari penyedia model ke `metadata` selama run berlangsung, frontend merendernya secara live, dan run bertahan dalam tool calls yang berjalan lama tanpa membakar timeout serverless.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 menggunakan `useRealtimeRun` dan membaca `run.metadata.partial` untuk merender respons streaming, dengan cara yang sama Anda akan merender chat completion - kecuali yang ini bertahan dari reload halaman penuh.385~386## Deploying387~388Deploys mengompilasi tasks Anda menjadi bundle berversi, membangun container, dan menukar traffic secara atomik. Runs lama yang sedang berlangsung terus menggunakan versi sebelumnya.389~390```bash391npx trigger.dev@latest deploy --env prod392```393~394Di CI Anda biasanya menghubungkan ini ke workflow yang sama yang mengirim aplikasi Anda: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~404Untuk environment preview, lewatkan `--env preview --branch ${{ github.head_ref }}` dan Trigger.dev membuat environment terisolasi per branch, mencerminkan cara Vercel menangani preview deployment.405~406## Self-Hosting vs Cloud407~408Trigger.dev open source di bawah lisensi Apache 2.0. Anda dapat self-host di platform container apa pun (Docker Compose, Kubernetes, Fly.io) atau menggunakan cloud terkelola di trigger.dev.409~410| Aspek | Cloud | Self-hosted |411|--------|-------|-------------|412| **Setup** | Daftar, jalankan `init` | Jalankan docker-compose atau Helm chart |413| **Scaling** | Otomatis | Tanggung jawab Anda |414| **Pricing** | Per run + per compute | Hanya biaya infra |415| **Compliance** | SOC 2 | Apa pun yang disediakan environment Anda |416| **Terbaik untuk** | Sebagian besar tim | Data residency ketat, infra custom |417~418SDK dan CLI identik di antara mode - Anda mengubah flag profile dan menunjuk ke instance Anda sendiri.419~420## Best Practices421~422### 1. Jaga payloads tetap kecil dan dapat diserialisasi423~424Lewatkan ID dan referensi, bukan objek lengkap. Tarik data di dalam task. Ini menjaga queue tetap kecil, payloads murah untuk dilog, dan memungkinkan Anda mengubah sumber data tanpa re-trigger.425~426### 2. Idempotency key di setiap panggilan eksternal427~428Gabungkan `idempotencyKey` di trigger task dengan idempotency keys di API vendor Anda (Stripe, OpenAI, dll.). Retries akan aman end-to-end.429~430### 3. Gunakan `triggerAndWait` untuk orkestrasi, bukan `Promise.all` dari triggers431~432Parent yang memanggil `triggerAndWait` secara tahan lama menyusun tasks anak. Parent yang trigger dan resolve segera kehilangan observability rantai.433~434### 4. Tag runs435~436Tambahkan `tags` ke trigger (`tags: ["user:123", "feature:onboarding"]`) sehingga Anda dapat memfilter dashboard dan management API berdasarkan dimensi bisnis.437~438### 5. Jaga `init` idempotent439~440Itu berjalan setiap cold start. Hindari migrations atau efek samping one-shot di sana.441~442## Kesimpulan443~444Trigger.dev menghapus kategori pekerjaan yang dulu memerlukan membangun sistem job dari awal. Anda menulis async TypeScript, memanggilnya dari mana saja, dan platform memberi Anda eksekusi tahan lama, scheduling, queues, retries, pembaruan real-time, dan pola human-in-the-loop out of the box.445~446Permukaan yang sama yang menggerakkan cron malam adalah permukaan yang menggerakkan AI agent multi-langkah yang streaming ke frontend dan jeda untuk review. Konvergensi itu yang membuat framework layak dilihat secara serius di 2026, baik Anda menjalankan SaaS yang membutuhkan pekerjaan background yang andal atau mengirim fitur AI yang bertahan setelah timeout serverless.447~448> **Checklist Memulai:**449>450> - [x] Daftar di trigger.dev atau jalankan stack Docker self-hosted451> - [x] `npx trigger.dev@latest init` di proyek Anda452> - [x] Definisikan task pertama Anda dengan `task({ id, run })`453> - [x] Trigger dari API Anda dan lihat run di dashboard454> - [x] Tambahkan `idempotencyKey` dan `concurrencyKey` untuk keamanan production455> - [x] Hubungkan `useRealtimeRun` ke komponen status456> - [x] Deploy dengan `trigger.dev deploy --env prod` dari CI457~
NORMAL · trigger-dev-background-jobs-guide.md [readonly]457 lines · :q to close