spinny:~/writing $ vim trigger-dev-background-jobs-guide.md
1~2Karamihan sa mga production application ay nangangailangan ng trabaho na hindi kasya sa request/response cycle: pagpapadala ng email, pagpoproseso ng uploads, pagpapatakbo ng AI pipelines, pag-sync ng third-party data, paggawa ng reports. Ang tradisyonal na sagot ay isang queue (Redis, SQS, RabbitMQ), isang fleet ng workers, isang scheduler, at isang marupok na hilera ng glue code na nasisira sa bawat deploy.3~4Pinipiga ng [Trigger.dev](https://trigger.dev) ang stack na iyon sa isang TypeScript SDK. Sumusulat ka ng functions, tinatawag mo sila kahit saan, at hinahawakan ng platform ang queueing, retries, observability, scheduling, at durable execution. Gumagana ang tasks hangga't kinakailangan - walang 10-second serverless timeout, walang nawawalang trabaho sa redeploys.5~6## Bakit Trigger.dev7~8Ang shift sa 2026 ay durable execution. Dapat makaligtas ang workflows sa restarts, crashes, deploys, at rate limits. Dapat din nilang i-stream ang progress sa UI nang real time at i-pause para sa human input. Muling itinayo ang Trigger.dev sa paligid ng mga kinakailangang ito sa version 3 at patuloy na pinapalawak ang AI infrastructure surface nito.9~10```mermaid11graph LR12 App[Iyong App] -->|trigger| API[Trigger.dev API]13 API --> Queue[Durable Queue]14 Queue --> Worker[Worker Container]15 Worker -->|run task| Task[Iyong Task code]16 Task -->|metadata| Realtime[Realtime Stream]17 Realtime --> UI[React UI]18 Worker --> Storage[Run State Store]19```20~21Simple lang ang model: tinutukoy mo ang tasks bilang exports, kinukuha sila ng SDK, ini-schedule at pinapatakbo sila ng platform sa isolated containers, at ang run state ay napapanatili upang maaari mong ipagpatuloy, subukang muli, at obserbahan.22~23## Pagsisimula24~25### Initialize ng project26~27```bash28npx trigger.dev@latest login29npx trigger.dev@latest init30```31~32Lumilikha ito ng isang `trigger.config.ts` file at isang `trigger/` directory na may sample na tasks. Ang config file ang pinagmumulan ng katotohanan para sa iyong project: aling mga directory ang naglalaman ng tasks, build settings, lifecycle hooks, at runtime options.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### Patakbuhin ang tasks lokal57~58```bash59npx trigger.dev@latest dev60```61~62Kumokonekta ang dev server sa cloud, nagrerehistro ng iyong tasks, at nag-stream ng runs sa pamamagitan ng iyong lokal na code. Naglalagay ka ng breakpoints sa iyong editor at tinatamaan ang mga ito sa totoong triggers - ang parehong loop na gagamitin mo sa anumang normal na Node.js project.63~64## Pagdedefine ng Task65~66Ang Task ay isang object na ini-export gamit ang isang natatanging `id` at isang `run` function. Sinusuri ng SDK ang exports sa kabuuan ng `dirs` at awtomatiko silang ini-rehistro.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~97Tatlong bagay na dapat tandaan:98~991. **Walang timeout sa run body.** Pinamamahalaan ng platform ang oras ng pagpapatupad sa pamamagitan ng `maxDuration` sa config, hindi sa runtime.1002. **Throws ay retries.** Hinuhuli ng SDK ang mga exception at muling pinapatakbo nang exponential backoff ayon sa `retry` policy.1013. **Pinapanatili ang return value.** Ang ibang tasks at ang frontend mo ay maaaring magbasa ng `run.output` mula sa kahit saan.102~103## Pag-trigger ng Tasks104~105Tatawagin mo ang task mula sa iyong backend, mga API route mo, o ibang 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 - gamitin para subaybayan o ipakita ang progress122```123~124Ang mga opsyon ay nagbubukas ng maraming gawi sa isang tawag:125~126- **`idempotencyKey`** - kung mayroon nang run na may parehong key, ibinabalik ng SDK ang umiiral na handle imbes na duplicate ang trabaho.127- **`concurrencyKey`** - ini-serialize ang runs na nagbabahagi ng key upang hindi mo lalagpas ang per-tenant rate limit.128- **`queue.concurrencyLimit`** - global cap para sa queue sa lahat ng keys.129- **`delay`** - inaiskedyul ang run sa isang oras sa hinaharap.130- **`ttl`** - kung hindi nagsimula ang run sa oras na iyon, awtomatikong mag-e-expire.131~132### Batch trigger133~134Para sa fan-out workloads, tumatanggap ang `batchTrigger` ng hanggang 500 items kada tawag at lumilikha ng isang run kada 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## Naka-iskedyul na Tasks146~147Ang Cron jobs ay nagiging first-class declarations. Ang schedule mismo ay isang hiwalay na object na maaari mong ikabit sa task nang maraming beses.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~167Para sa per-tenant schedules - sabihin nating, isang cron kada customer - dynamic mong nilikha sila sa pamamagitan ng 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~181Ginagawang idempotent ng `deduplicationKey` ang tawag: ang muling pagpapatakbo ng parehong code sa deploy time ay hindi nag-iipon ng duplicate na schedules.182~183## Queues, Concurrency, at Idempotency184~185Tatlong primitives ang sumasaklaw sa karamihan ng pangangailangan ng rate-limiting at ordering.186~187```mermaid188graph TB189 Trigger[trigger payload] --> IK{idempotencyKey<br/>nakita?}190 IK -->|oo| Reuse[Ibalik ang umiiral na run]191 IK -->|hindi| CK[concurrencyKey bucket]192 CK --> Q[Queue na may<br/>concurrencyLimit]193 Q -->|slot available| Run[Patakbuhin ang task]194 Q -->|slots puno| Wait[Maghintay sa queue]195```196~197Karaniwang pattern: isang queue kada tenant na may maliit na per-key concurrency upang igalang ang rate limit ng vendor, plus isang idempotency key upang gawing ligtas ang 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 at Long-Running Work211~212Ang Tasks ay maaaring i-pause nang hindi nagpapanatili ng connection o nagsusunog ng compute. Pinapanatili ng platform ang state at ipinagpapatuloy ang function kapag natapos ang 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~229Ang `triggerAndWait` ay ang killer feature: nagti-trigger ito ng isang child task at sini-suspend ang parent hanggang sa matapos ang child. Bumubuo ka ng tasks gaya ng async functions, ngunit ang orchestration ay tumatakbo nang matatag sa loob ng mga araw o linggo.230~231### Human-in-the-loop sa `wait.forToken`232~233Para sa approval flows at AI gates, ang `wait.forToken` ay nag-pa-pause hanggang sa tumawag pabalik ang iyong application na may resulta.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~258Binubuksan ng editor ang isang UI, sinusuri ang draft, ki-click ang Approve, at kinukumpleto ng iyong backend ang token. Ipinagpapatuloy ng task kung saan ito tumigil - kahit na lumipas ang oras o mga araw.259~260## Lifecycle Hooks261~262Maaari mong ikabit ang `init`, `onStart`, `onSuccess`, at `onFailure` sa isang task o globally sa `trigger.config.ts`. Gamitin sila para sa tracing, error reporting, at shared 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~279Tumatakbo ang `init` minsan kada worker container sa boot, hindi kada run, kaya ito ang tamang lugar upang i-set up ang clients at pools.280~281## Realtime sa Frontend282~283Ang Trigger.dev ay nag-pu-publish ng mga pagbabago ng run state - status, metadata, output - sa pamamagitan ng streaming API. Ang React hooks ay nag-su-subscribe sa stream na iyon at awtomatikong nagre-render muli.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~336Bumubuo ka ng public access token sa server-side, scoped sa isang partikular na run, at ipinapadala ito sa client. Hinahawakan ng hook ang auth, reconnection, at incremental updates.337~338Para sa trigger-and-subscribe sa isang hakbang: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 at Streaming354~355Ang Trigger.dev ay naging isang popular na runtime para sa AI agents dahil ang parehong primitives - durable execution, retries, waits, real-time metadata, human-in-the-loop - ay eksaktong kailangan ng agents. Nag-stream ka ng tokens mula sa isang model provider sa `metadata` habang nangyayari ang run, ire-render ang frontend ang mga ito nang live, at nakaligtas ang run sa long-running tool calls nang hindi nagsusunog ng serverless timeout.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~384Gumagamit ang frontend ng `useRealtimeRun` at binabasa ang `run.metadata.partial` upang i-render ang streaming response, sa parehong paraan na ire-render mo ang isang chat completion - maliban na ang isang ito ay nakaligtas sa isang full page reload.385~386## Pag-deploy387~388Ang mga deploys ay nagko-compile ng iyong tasks sa isang versioned bundle, bumubuo ng container, at atomically pinagbabago ang trapiko. Ang lumang in-flight runs ay patuloy na gumagamit ng nakaraang version.389~390```bash391npx trigger.dev@latest deploy --env prod392```393~394Sa CI ay karaniwang ikinakabit mo ito sa parehong workflow na nagpapadala ng iyong 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~404Para sa preview environments, ipasa ang `--env preview --branch ${{ github.head_ref }}` at ang Trigger.dev ay lumilikha ng isolated environment kada branch, na sumasalamin sa kung paano hawakan ng Vercel ang preview deployments.405~406## Self-Hosting vs Cloud407~408Ang Trigger.dev ay open source sa ilalim ng Apache 2.0 license. Maaari kang mag-self-host sa anumang container platform (Docker Compose, Kubernetes, Fly.io) o gamitin ang managed cloud sa trigger.dev.409~410| Aspeto | Cloud | Self-hosted |411|--------|-------|-------------|412| **Setup** | Mag-sign up, patakbuhin ang `init` | Patakbuhin ang docker-compose o Helm chart |413| **Scaling** | Awtomatiko | Iyong responsibilidad |414| **Pricing** | Kada run + kada compute | Gastos lamang ng infra |415| **Compliance** | SOC 2 | Kahit anong ibinibigay ng iyong environment |416| **Pinakamahusay para sa** | Karamihan ng teams | Mahigpit na data residency, custom infra |417~418Ang SDK at CLI ay magkapareho sa pagitan ng modes - binabago mo ang isang profile flag at itinuturo sa iyong sariling instance.419~420## Best Practices421~422### 1. Panatilihing maliit at serializable ang payloads423~424Ipasa ang IDs at references, hindi buong objects. Hilahin ang data sa loob ng task. Pinapanatili nitong maliit ang queue, mura ang payloads na i-log, at pinapayagan kang baguhin ang data source nang hindi muling nag-tri-trigger.425~426### 2. Idempotency keys sa bawat external call427~428Pagsamahin ang `idempotencyKey` sa task trigger sa idempotency keys sa iyong vendor APIs (Stripe, OpenAI, atbp.). Magiging ligtas ang retries sa end-to-end.429~430### 3. Gamitin ang `triggerAndWait` para sa orchestration, hindi `Promise.all` ng triggers431~432Ang isang parent na tumatawag ng `triggerAndWait` ay durable na nagsasama-sama ng child tasks. Ang isang parent na nag-tri-trigger at nag-re-resolve agad ay nawawalan ng observability ng chain.433~434### 4. I-tag ang runs435~436Magdagdag ng `tags` sa triggers (`tags: ["user:123", "feature:onboarding"]`) upang ma-filter mo ang dashboard at management API ayon sa business dimensions.437~438### 5. Panatilihing idempotent ang `init`439~440Tumatakbo ito sa bawat cold start. Iwasan ang migrations o one-shot side effects doon.441~442## Konklusyon443~444Inaalis ng Trigger.dev ang mga kategorya ng trabaho na dating nangangailangan ng pagbuo ng job system mula sa simula. Sumusulat ka ng async TypeScript, tinatawag mo ito kahit saan, at binibigyan ka ng platform ng durable execution, scheduling, queues, retries, real-time updates, at human-in-the-loop patterns out of the box.445~446Ang parehong surface na nagpapatakbo ng nightly cron ay ang surface na nagpapatakbo ng multi-step AI agent na nag-stream sa frontend at nag-pa-pause para sa review. Ang convergence na iyon ang nagbibigay ng halaga sa framework para sa seryosong tingin sa 2026, kahit nagpapatakbo ka ng SaaS na nangangailangan ng maaasahang background work o nagpapadala ng AI features na nakaligtas sa serverless timeout.447~448> **Checklist sa Pagsisimula:**449>450> - [x] Mag-sign up sa trigger.dev o patakbuhin ang self-hosted Docker stack451> - [x] `npx trigger.dev@latest init` sa iyong project452> - [x] Tukuyin ang iyong unang task gamit ang `task({ id, run })`453> - [x] I-trigger ito mula sa iyong API at panoorin ang run sa dashboard454> - [x] Magdagdag ng `idempotencyKey` at `concurrencyKey` para sa kaligtasan ng production455> - [x] Ikabit ang `useRealtimeRun` sa isang status component456> - [x] I-deploy gamit ang `trigger.dev deploy --env prod` mula sa CI457~
NORMAL · trigger-dev-background-jobs-guide.md [readonly]457 lines · :q to close