spinny:~/writing $ vim trigger-dev-background-jobs-guide.md
1~2Kebanyakan aplikasi pengeluaran memerlukan kerja yang tidak sesuai dengan kitaran request/response: menghantar e-mel, memproses muat naik, menjalankan pipeline AI, menyegerakkan data pihak ketiga, menjana laporan. Jawapan tradisional ialah satu queue (Redis, SQS, RabbitMQ), satu armada worker, satu scheduler, dan satu timbunan kod glue rapuh yang pecah pada setiap deploy.3~4[Trigger.dev](https://trigger.dev) memampatkan stack itu menjadi satu TypeScript SDK. Anda menulis fungsi, memanggilnya dari mana-mana, dan platform mengendalikan queueing, retries, observability, scheduling, dan pelaksanaan tahan lasak. Tasks berjalan selama yang diperlukan - tiada timeout serverless 10 saat, tiada kerja hilang pada redeploys.5~6## Mengapa Trigger.dev7~8Peralihan pada 2026 ialah pelaksanaan tahan lasak. Workflow mesti bertahan daripada restart, crash, deploy, dan rate limit. Mereka juga mesti melakukan streaming kemajuan ke UI dalam masa nyata dan jeda untuk input manusia. Trigger.dev dibina semula sekitar keperluan ini dengan versi 3 dan terus mengembangkan permukaan infrastruktur AInya.9~10```mermaid11graph LR12 App[Aplikasi anda] -->|trigger| API[Trigger.dev API]13 API --> Queue[Queue Tahan Lasak]14 Queue --> Worker[Worker Container]15 Worker -->|run task| Task[Kod Task anda]16 Task -->|metadata| Realtime[Stream Realtime]17 Realtime --> UI[React UI]18 Worker --> Storage[Run State Store]19```20~21Modelnya mudah: anda mendefinisikan tasks sebagai exports, SDK mengambilnya, platform menjadualkan dan menjalankannya dalam container terpencil, dan keadaan run dikekalkan supaya anda boleh menyambung semula, mencuba semula, dan memerhati.22~23## Bermula24~25### Mulakan projek26~27```bash28npx trigger.dev@latest login29npx trigger.dev@latest init30```31~32Ini mencipta fail `trigger.config.ts` dan direktori `trigger/` dengan tasks contoh. Fail config adalah sumber kebenaran untuk projek anda: direktori mana mengandungi tasks, tetapan build, lifecycle hooks, dan opsyen 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 setempat57~58```bash59npx trigger.dev@latest dev60```61~62Pelayan dev menyambung ke awan, mendaftarkan tasks anda, dan melakukan streaming runs melalui kod tempatan anda. Anda menetapkan breakpoints dalam editor anda dan memukul mereka pada trigger sebenar - gelung yang sama yang anda akan gunakan dalam mana-mana projek Node.js biasa.63~64## Mendefinisikan Task65~66Task ialah objek yang dieksport dengan `id` unik dan fungsi `run`. SDK memeriksa exports merentasi `dirs` dan mendaftarkannya secara automatik.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 perkara yang perlu diperhatikan:98~991. **Tiada timeout dalam badan run.** Platform menguruskan masa pelaksanaan melalui `maxDuration` dalam config, bukan dalam runtime.1002. **Throws ialah retries.** SDK menangkap pengecualian dan menjalankan semula dengan exponential backoff mengikut polisi `retry`.1013. **Nilai pulangan dikekalkan.** Tasks lain dan frontend anda boleh membaca `run.output` dari mana-mana.102~103## Triggering Tasks104~105Anda memanggil task dari backend anda, route API anda, 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 menjejaki atau memaparkan kemajuan122```123~124Pilihan membuka banyak tingkah laku dalam satu panggilan:125~126- **`idempotencyKey`** - jika run dengan kekunci yang sama sudah wujud, SDK mengembalikan handle sedia ada bukannya menduplikasi kerja.127- **`concurrencyKey`** - mengsiri runs yang berkongsi kekunci supaya anda tidak melebihi rate limit per-tenant.128- **`queue.concurrencyLimit`** - cap global untuk queue merentasi semua kekunci.129- **`delay`** - menjadualkan run untuk masa akan datang.130- **`ttl`** - jika run belum bermula menjelang masa itu, ia tamat tempoh secara automatik.131~132### Batch trigger133~134Untuk beban kerja fan-out, `batchTrigger` menerima sehingga 500 item setiap panggilan dan mencipta satu run setiap 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 Berjadual146~147Cron jobs menjadi pengisytiharan kelas pertama. Schedule itu sendiri ialah objek berasingan yang anda boleh lampirkan kepada 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 - katakan, satu cron setiap pelanggan - anda menciptanya secara dinamik 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` menjadikan panggilan idempotent: menjalankan semula kod yang sama pada masa deploy tidak menyusun schedules pendua.182~183## Queues, Concurrency, dan Idempotency184~185Tiga primitif meliputi kebanyakan keperluan rate-limiting dan susunan.186~187```mermaid188graph TB189 Trigger[trigger payload] --> IK{idempotencyKey<br/>dilihat?}190 IK -->|ya| Reuse[Pulangkan run sedia 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 dalam queue]195```196~197Corak biasa: satu queue per tenant dengan concurrency per-key kecil untuk menghormati rate limit vendor, ditambah dengan idempotency key untuk membuat retries selamat.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 Kerja Berjangka Panjang211~212Tasks boleh dihentikan tanpa memegang sambungan atau membakar compute. Platform mengekalkan keadaan dan menyambung semula fungsi apabila 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 ciri pembunuh: ia mencetuskan child task dan menggantung parent sehingga child selesai. Anda menggubah tasks seperti fungsi async, tetapi orchestration berjalan tahan lasak selama beberapa hari atau minggu.230~231### Human-in-the-loop dengan `wait.forToken`232~233Untuk aliran kelulusan dan AI gates, `wait.forToken` jeda sehingga aplikasi anda memanggil semula 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, menyemak draft, klik Lulus, dan backend anda melengkapkan token. Task menyambung dari tempat ia berhenti - walaupun jam atau hari telah berlalu.259~260## Lifecycle Hooks261~262Anda boleh melampirkan `init`, `onStart`, `onSuccess`, dan `onFailure` ke task atau secara global dalam `trigger.config.ts`. Gunakannya untuk tracing, error reporting, dan setup dikongsi.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 pada boot, bukan per run, jadi ia adalah tempat yang betul untuk menyediakan clients dan pools.280~281## Realtime dalam Frontend282~283Trigger.dev menerbitkan perubahan keadaan run - status, metadata, output - melalui streaming API. React hooks melanggan stream itu dan re-render secara automatik.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 menjana public access token di sisi pelayan, scoped ke run tertentu, dan menghantarnya ke client. Hook mengendalikan auth, sambungan semula, dan kemas kini tambahan.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 popular untuk AI agents kerana primitif yang sama - pelaksanaan tahan lasak, retries, waits, real-time metadata, human-in-the-loop - adalah tepat apa yang diperlukan agents. Anda streaming token dari penyedia model ke `metadata` semasa run berlaku, frontend merendernya secara langsung, dan run terselamat melalui tool calls berjangka panjang 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 tindak balas streaming, dengan cara yang sama anda akan merender chat completion - kecuali yang ini bertahan dari muat semula halaman penuh.385~386## Deploying387~388Deploys mengkompilasi tasks anda kepada bundle berversi, membina container, dan menukar trafik secara atomik. runs lama dalam penerbangan terus menggunakan versi sebelumnya.389~390```bash391npx trigger.dev@latest deploy --env prod392```393~394Dalam CI anda biasanya menyambungkan ini ke workflow yang sama yang menghantar 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 persekitaran preview, hantar `--env preview --branch ${{ github.head_ref }}` dan Trigger.dev mencipta persekitaran terpencil per branch, mencerminkan cara Vercel mengendalikan preview deployments.405~406## Self-Hosting vs Cloud407~408Trigger.dev adalah sumber terbuka di bawah lesen Apache 2.0. Anda boleh self-host pada mana-mana platform container (Docker Compose, Kubernetes, Fly.io) atau menggunakan cloud terurus di trigger.dev.409~410| Aspek | Cloud | Self-hosted |411|--------|-------|-------------|412| **Setup** | Daftar, jalankan `init` | Jalankan docker-compose atau Helm chart |413| **Scaling** | Automatik | Tanggungjawab anda |414| **Pricing** | Per run + per compute | Hanya kos infra |415| **Compliance** | SOC 2 | Apa sahaja yang persekitaran anda sediakan |416| **Terbaik untuk** | Kebanyakan pasukan | Kediaman data ketat, infra tersuai |417~418SDK dan CLI adalah sama antara mod - anda menukar bendera profil dan menunjuk ke instance anda sendiri.419~420## Best Practices421~422### 1. Pastikan payloads kecil dan boleh diserialisasi423~424Hantar IDs dan rujukan, bukan objek penuh. Tarik data di dalam task. Ini menjadikan queue kecil, payloads murah untuk dilog, dan membolehkan anda menukar sumber data tanpa mencetus semula.425~426### 2. Idempotency keys pada setiap panggilan luar427~428Gabungkan `idempotencyKey` pada trigger task dengan idempotency keys pada API vendor anda (Stripe, OpenAI, dll.). retries akan selamat dari hujung ke hujung.429~430### 3. Gunakan `triggerAndWait` untuk orchestration, bukan `Promise.all` daripada triggers431~432Parent yang memanggil `triggerAndWait` secara tahan lasak menggubah child tasks. Parent yang trigger dan menyelesaikan dengan segera kehilangan observability rantaian.433~434### 4. Tag runs435~436Tambahkan `tags` ke triggers (`tags: ["user:123", "feature:onboarding"]`) supaya anda boleh menapis dashboard dan management API mengikut dimensi perniagaan.437~438### 5. Pastikan `init` idempotent439~440Ia berjalan pada setiap cold start. Elakkan migrations atau kesan sampingan one-shot di sana.441~442## Kesimpulan443~444Trigger.dev menghapuskan kategori kerja yang dahulunya memerlukan membina sistem job dari awal. Anda menulis async TypeScript, anda memanggilnya dari mana-mana, dan platform memberi anda pelaksanaan tahan lasak, scheduling, queues, retries, kemas kini real-time, dan corak human-in-the-loop di luar kotak.445~446Permukaan yang sama yang menggerakkan cron malam adalah permukaan yang menggerakkan multi-step AI agent yang streaming ke frontend dan jeda untuk semakan. Konvergensi itulah yang menjadikan framework berbaloi untuk pandangan serius pada 2026, sama ada anda menjalankan SaaS yang memerlukan kerja background yang boleh dipercayai atau menghantar ciri AI yang bertahan melepasi serverless timeout.447~448> **Senarai Semak Memulakan:**449>450> - [x] Daftar di trigger.dev atau jalankan stack Docker self-hosted451> - [x] `npx trigger.dev@latest init` dalam projek anda452> - [x] Tentukan task pertama anda dengan `task({ id, run })`453> - [x] Trigger ia dari API anda dan tonton run dalam dashboard454> - [x] Tambah `idempotencyKey` dan `concurrencyKey` untuk keselamatan production455> - [x] Sambungkan `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