spinny:~/writing $ vim trigger-dev-background-jobs-guide.md
1~2Majoritatea aplicatiilor de productie au nevoie de munca care nu se incadreaza in ciclul cerere/raspuns: trimiterea de email-uri, procesarea uploadurilor, rularea pipeline-urilor AI, sincronizarea datelor de la terti, generarea de rapoarte. Raspunsul traditional este o coada (Redis, SQS, RabbitMQ), o flota de workeri, un scheduler si o gramada fragila de cod glue care se sparge la fiecare deploy.3~4[Trigger.dev](https://trigger.dev) reduce acel stack la un singur SDK TypeScript. Scrii functii, le apelezi de oriunde, iar platforma se ocupa de queueing, retries, observability, scheduling si executie durabila. Tasks ruleaza atat de mult cat este nevoie - fara timeout serverless de 10 secunde, fara munca pierduta la redeploy-uri.5~6## De ce Trigger.dev7~8Schimbarea din 2026 este executia durabila. Workflow-urile trebuie sa supravietuiasca restartarilor, crash-urilor, deploy-urilor si rate limit-urilor. Ele trebuie de asemenea sa transmita progresul catre UI in timp real si sa se opreasca pentru input uman. Trigger.dev a fost reconstruit in jurul acestor cerinte cu versiunea 3 si continua sa-si extinda suprafata de infrastructura AI.9~10```mermaid11graph LR12 App[Aplicatia ta] -->|trigger| API[Trigger.dev API]13 API --> Queue[Coada Durabila]14 Queue --> Worker[Container Worker]15 Worker -->|run task| Task[Codul tau Task]16 Task -->|metadata| Realtime[Stream Realtime]17 Realtime --> UI[React UI]18 Worker --> Storage[Store Stare Run]19```20~21Modelul este simplu: definesti tasks ca exports, SDK-ul le ridica, platforma le programeaza si le ruleaza in containere izolate, iar starea run-ului este pastrata astfel incat sa poti relua, reincerca si observa.22~23## Inceperea24~25### Initializeaza un proiect26~27```bash28npx trigger.dev@latest login29npx trigger.dev@latest init30```31~32Aceasta creeaza un fisier `trigger.config.ts` si un director `trigger/` cu tasks de exemplu. Fisierul config este sursa adevarului pentru proiectul tau: ce directoare contin tasks, setarile de build, lifecycle hooks si optiunile de 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### Ruleaza tasks local57~58```bash59npx trigger.dev@latest dev60```61~62Serverul dev se conecteaza la cloud, inregistreaza task-urile tale si transmite runs prin codul tau local. Setezi breakpoints in editorul tau si le atingi pe trigger-uri reale - acelasi loop pe care l-ai folosi in orice proiect normal Node.js.63~64## Definirea unui Task65~66Un task este un obiect exportat cu un `id` unic si o functie `run`. SDK-ul inspecteaza exporturile prin `dirs` si le inregistreaza automat.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~97Trei lucruri de retinut:98~991. **Niciun timeout in body-ul run.** Platforma gestioneaza timpul de executie prin `maxDuration` in config, nu in runtime.1002. **Throws sunt retries.** SDK-ul prinde exceptiile si reruleaza cu backoff exponential conform politicii `retry`.1013. **Valoarea returnata este pastrata.** Alte tasks si frontend-ul tau pot citi `run.output` de oriunde.102~103## Triggerarea Tasks-urilor104~105Apelezi un task din backend-ul tau, rutele API sau un alt 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 - foloseste pentru a urmari sau afisa progresul122```123~124Optiunile deblocheaza mult comportament intr-un singur apel:125~126- **`idempotencyKey`** - daca un run cu aceeasi cheie exista deja, SDK-ul returneaza handle-ul existent in loc sa duplice munca.127- **`concurrencyKey`** - serializeaza runs care impart cheia astfel incat sa nu depasesti un rate limit per-tenant.128- **`queue.concurrencyLimit`** - cap global pentru coada in toate cheile.129- **`delay`** - programeaza run-ul pentru un timp viitor.130- **`ttl`** - daca run-ul nu a inceput pana atunci, expira automat.131~132### Batch trigger133~134Pentru workload-uri fan-out, `batchTrigger` accepta pana la 500 de items pe apel si creeaza un 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 Programate146~147Joburile cron devin declaratii de prima clasa. Schedule-ul in sine este un obiect separat pe care il poti atasa la un task de mai multe ori.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~167Pentru schedules per-tenant - sa zicem, un cron per client - le creezi dinamic prin 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` face apelul idempotent: rerularea aceluiasi cod la momentul deploy-ului nu acumuleaza schedules duplicate.182~183## Cozi, concurrency si idempotency184~185Trei primitive acopera majoritatea nevoilor de rate-limiting si ordonare.186~187```mermaid188graph TB189 Trigger[trigger payload] --> IK{idempotencyKey<br/>vazuta?}190 IK -->|da| Reuse[Returneaza run existent]191 IK -->|nu| CK[bucket concurrencyKey]192 CK --> Q[Coada cu<br/>concurrencyLimit]193 Q -->|slot disponibil| Run[Ruleaza task]194 Q -->|sloturi pline| Wait[Asteapta in coada]195```196~197Un pattern comun: o coada per tenant cu o concurrency mica per-cheie pentru a respecta rate limit-ul unui vendor, plus o cheie de idempotency pentru a face retries sigure.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## Asteptari si munca de lunga durata211~212Tasks pot face pauza fara a tine o conexiune sau a arde compute. Platforma pastreaza starea si reia functia cand asteptarea se termina.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` este caracteristica killer: declanseaza un task copil si suspenda parintele pana cand copilul se termina. Compui tasks ca functii async, dar orchestrarea ruleaza durabil pe parcursul zilelor sau saptamanilor.230~231### Human-in-the-loop cu `wait.forToken`232~233Pentru fluxuri de aprobare si gates AI, `wait.forToken` face pauza pana cand aplicatia ta apeleaza inapoi cu un rezultat.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~258Editorul deschide un UI, revizuieste schita, da clic pe Approve, iar backend-ul tau finalizeaza tokenul. Task-ul preia de unde a ramas - chiar daca au trecut ore sau zile.259~260## Lifecycle Hooks261~262Poti atasa `init`, `onStart`, `onSuccess` si `onFailure` la un task sau global in `trigger.config.ts`. Foloseste-le pentru tracing, error reporting si setup partajat.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` ruleaza o data per worker container la boot, nu per run, deci este locul potrivit pentru a configura clienti si pool-uri.280~281## Realtime in Frontend282~283Trigger.dev publica modificarile starii run - status, metadata, output - peste un API streaming. Hook-urile React se aboneaza la acel stream si re-renderizeaza automat.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~336Generezi public access token-ul pe partea de server, scoped la un run specific, si il trimiti la client. Hook-ul gestioneaza auth, reconectarea si actualizarile incrementale.337~338Pentru trigger-and-subscribe intr-un singur pas: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## Agenti AI si Streaming354~355Trigger.dev a devenit un runtime popular pentru agentii AI deoarece aceleasi primitive - executie durabila, retries, asteptari, metadata in timp real, human-in-the-loop - sunt exact ceea ce au nevoie agentii. Stream-ezi tokens de la un model provider in `metadata` in timp ce run se intampla, frontend-ul le renderizeaza live, iar run supravietuieste tool calls de lunga durata fara a arde un 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-ul foloseste `useRealtimeRun` si citeste `run.metadata.partial` pentru a renderiza raspunsul streaming, in acelasi mod in care ai renderiza un chat completion - cu exceptia faptului ca acesta supravietuieste unei reincarcari complete a paginii.385~386## Deployment387~388Deploy-urile compileaza task-urile tale intr-un bundle versionat, construiesc un container si schimba traficul atomic. Runs vechi in zbor continua sa foloseasca versiunea anterioara.389~390```bash391npx trigger.dev@latest deploy --env prod392```393~394In CI conectezi de obicei aceasta la acelasi workflow care livreaza aplicatia ta: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~404Pentru medii preview, treci `--env preview --branch ${{ github.head_ref }}` si Trigger.dev creeaza un mediu izolat per branch, oglindind cum Vercel gestioneaza preview deployment-urile.405~406## Self-Hosting vs Cloud407~408Trigger.dev este open source sub licenta Apache 2.0. Poti self-host pe orice platforma container (Docker Compose, Kubernetes, Fly.io) sau folosi cloud-ul gestionat la trigger.dev.409~410| Aspect | Cloud | Self-hosted |411|--------|-------|-------------|412| **Setup** | Inregistrare, executie `init` | Executie docker-compose sau Helm chart |413| **Scaling** | Automat | Responsabilitatea ta |414| **Pricing** | Per run + per compute | Doar costul infrastructurii |415| **Compliance** | SOC 2 | Ce ofera mediul tau |416| **Ideal pentru** | Majoritatea echipelor | Rezidenta stricta a datelor, infra custom |417~418SDK-ul si CLI-ul sunt identice intre moduri - schimbi un flag profile si pointezi la propria ta instanta.419~420## Best Practices421~422### 1. Mentine payloads-urile mici si serializabile423~424Trimite ID-uri si referinte, nu obiecte complete. Trage datele in interiorul task-ului. Aceasta mentine coada mica, payloads ieftine de logat si iti permite sa schimbi sursa de date fara a re-trigger-a.425~426### 2. Idempotency keys pe fiecare apel extern427~428Combina `idempotencyKey` pe trigger-ul task-ului cu chei de idempotency pe API-urile vendor-ilor (Stripe, OpenAI, etc.). Retries vor fi sigure end-to-end.429~430### 3. Foloseste `triggerAndWait` pentru orchestrare, nu `Promise.all` de trigger-e431~432Un parinte care apeleaza `triggerAndWait` compune durabil tasks copii. Un parinte care declanseaza si rezolva imediat pierde observabilitatea lantului.433~434### 4. Tag runs435~436Adauga `tags` la trigger-e (`tags: ["user:123", "feature:onboarding"]`) astfel incat sa poti filtra dashboard-ul si management API dupa dimensiuni de business.437~438### 5. Mentine `init` idempotent439~440Ruleaza la fiecare cold start. Evita migrations sau efecte secundare one-shot acolo.441~442## Concluzie443~444Trigger.dev elimina categoriile de munca care odata necesitau sa construiesti un sistem de joburi de la zero. Scrii async TypeScript, il apelezi de oriunde, iar platforma iti ofera executie durabila, scheduling, cozi, retries, actualizari in timp real si pattern-uri human-in-the-loop out of the box.445~446Aceeasi suprafata care alimenteaza un cron de noapte este suprafata care alimenteaza un agent AI multi-step care transmite la frontend si face pauza pentru revizuire. Aceasta convergenta face framework-ul demn de o privire serioasa in 2026, fie ca rulezi un SaaS care are nevoie de munca de fundal fiabila, fie ca livrezi caracteristici AI care supravietuiesc unui timeout serverless.447~448> **Checklist Inceput:**449>450> - [x] Inregistreaza-te pe trigger.dev sau ruleaza stack-ul Docker self-hosted451> - [x] `npx trigger.dev@latest init` in proiectul tau452> - [x] Defineste primul task cu `task({ id, run })`453> - [x] Trigger-eaza-l din API-ul tau si vezi run-ul in dashboard454> - [x] Adauga `idempotencyKey` si `concurrencyKey` pentru siguranta in productie455> - [x] Conecteaza `useRealtimeRun` la o componenta de status456> - [x] Deploy cu `trigger.dev deploy --env prod` din CI457~
NORMAL · trigger-dev-background-jobs-guide.md [readonly]457 lines · :q to close