spinny:~/writing $ vim trigger-dev-background-jobs-guide.md
1~2La maggior parte delle applicazioni in produzione ha bisogno di lavoro che non rientra nel ciclo richiesta/risposta: invio di email, processamento di upload, esecuzione di pipeline AI, sincronizzazione di dati di terze parti, generazione di report. La risposta tradizionale e una coda (Redis, SQS, RabbitMQ), una flotta di worker, uno scheduler e una pila fragile di codice glue che si rompe a ogni deploy.3~4[Trigger.dev](https://trigger.dev) collassa quello stack in un singolo SDK TypeScript. Scrivi funzioni, le chiami da qualsiasi punto e la piattaforma gestisce code, retry, osservabilita, scheduling ed esecuzione durabile. I task girano per il tempo necessario - nessun timeout serverless da 10 secondi, nessun lavoro perso al redeploy.5~6## Perche Trigger.dev7~8Lo spostamento del 2026 e l'esecuzione durabile. I workflow devono sopravvivere a riavvii, crash, deploy e rate limit. Devono anche fare streaming dei progressi verso la UI in tempo reale e mettersi in pausa per l'input umano. Trigger.dev e stato ricostruito attorno a questi requisiti con la versione 3 e continua a espandere la sua superficie di infrastruttura per AI.9~10```mermaid11graph LR12 App[La tua App] -->|trigger| API[API Trigger.dev]13 API --> Queue[Coda Durabile]14 Queue --> Worker[Container Worker]15 Worker -->|esegue task| Task[Codice del tuo Task]16 Task -->|metadata| Realtime[Stream Realtime]17 Realtime --> UI[UI React]18 Worker --> Storage[Store Stato Run]19```20~21Il modello e semplice: definisci i task come export, l'SDK li raccoglie, la piattaforma li schedula e li esegue in container isolati e lo stato del run viene persistito cosi puoi riprendere, fare retry e osservare.22~23## Per Iniziare24~25### Inizializza un progetto26~27```bash28npx trigger.dev@latest login29npx trigger.dev@latest init30```31~32Questo crea un file `trigger.config.ts` e una directory `trigger/` con task di esempio. Il file di config e la fonte di verita per il tuo progetto: quali directory contengono i task, le impostazioni di build, i lifecycle hook e le opzioni di 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### Esegui i task in locale57~58```bash59npx trigger.dev@latest dev60```61~62Il dev server si connette al cloud, registra i tuoi task e fa streaming dei run attraverso il tuo codice locale. Imposti i breakpoint nel tuo editor e li raggiungi sui trigger reali - lo stesso loop che useresti in qualsiasi normale progetto Node.js.63~64## Definire un Task65~66Un task e un oggetto esportato con un `id` unico e una funzione `run`. L'SDK ispeziona gli export attraverso `dirs` e li registra automaticamente.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~97Tre cose da notare:98~991. **Nessun timeout nel corpo del run.** La piattaforma gestisce il tempo di esecuzione tramite `maxDuration` nel config, non nel runtime.1002. **Le throw sono retry.** L'SDK cattura le eccezioni e ri-esegue con backoff esponenziale secondo la policy `retry`.1013. **Il valore di ritorno viene persistito.** Altri task e il tuo frontend possono leggere `run.output` da qualsiasi punto.102~103## Triggerare i Task104~105Chiami un task dal tuo backend, dalle tue route API o da un altro 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 - usalo per tracciare o mostrare il progresso122```123~124Le opzioni sbloccano molto comportamento in una singola chiamata:125~126- **`idempotencyKey`** - se un run con la stessa key esiste gia, l'SDK restituisce l'handle esistente invece di duplicare il lavoro.127- **`concurrencyKey`** - serializza i run che condividono la key cosi non superi il rate limit per-tenant.128- **`queue.concurrencyLimit`** - cap globale per la coda attraverso tutte le key.129- **`delay`** - schedula il run per un tempo futuro.130- **`ttl`** - se il run non e partito entro quel momento, lo fa scadere automaticamente.131~132### Batch trigger133~134Per workload di fan-out, `batchTrigger` accetta fino a 500 elementi per chiamata e crea un run per elemento.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## Task Schedulati146~147I cron job diventano dichiarazioni di prima classe. Lo schedule stesso e un oggetto separato che puoi attaccare a un task piu volte.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~167Per schedule per-tenant - diciamo, un cron per cliente - li crei dinamicamente attraverso la 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~181La `deduplicationKey` rende la chiamata idempotente: rieseguire lo stesso codice al deploy time non accatasta schedule duplicati.182~183## Code, Concorrenza e Idempotenza184~185Tre primitive coprono la maggior parte delle esigenze di rate-limiting e ordering.186~187```mermaid188graph TB189 Trigger[trigger payload] --> IK{idempotencyKey<br/>vista?}190 IK -->|si| Reuse[Restituisci run esistente]191 IK -->|no| CK[bucket concurrencyKey]192 CK --> Q[Coda con<br/>concurrencyLimit]193 Q -->|slot disponibile| Run[Esegui task]194 Q -->|slot pieni| Wait[Attendi in coda]195```196~197Un pattern comune: una coda per tenant con una piccola concorrenza per-key per rispettare il rate limit di un vendor, piu una idempotency key per rendere i retry sicuri.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## Attese e Lavoro a Lunga Durata211~212I task possono mettersi in pausa senza tenere una connessione o bruciare compute. La piattaforma persiste lo stato e riprende la funzione quando l'attesa si completa.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` e la feature decisiva: triggera un task figlio e sospende il padre finche il figlio non si completa. Componi i task come funzioni async, ma l'orchestrazione gira durabilmente attraverso giorni o settimane.230~231### Human-in-the-loop con `wait.forToken`232~233Per flussi di approvazione e gate AI, `wait.forToken` mette in pausa finche la tua applicazione non risponde con un risultato.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~258L'editor apre una UI, rivede la bozza, clicca Approva e il tuo backend completa il token. Il task riprende da dove si era fermato - anche se sono passate ore o giorni.259~260## Lifecycle Hook261~262Puoi attaccare `init`, `onStart`, `onSuccess` e `onFailure` a un task o globalmente in `trigger.config.ts`. Usali per tracing, error reporting e setup condiviso.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` gira una volta per container worker al boot, non per run, quindi e il posto giusto per configurare client e pool.280~281## Realtime nel Frontend282~283Trigger.dev pubblica i cambi di stato del run - status, metadata, output - su una API in streaming. Gli hook React si sottoscrivono a quello stream e fanno re-render automaticamente.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~336Generi il public access token lato server, scoped a uno specifico run, e lo spedisci al client. L'hook gestisce auth, riconnessione e aggiornamenti incrementali.337~338Per trigger-and-subscribe in un colpo solo: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 e Streaming354~355Trigger.dev e diventato un runtime popolare per gli agenti AI perche le stesse primitive - esecuzione durabile, retry, attese, metadata in tempo reale, human-in-the-loop - sono esattamente quello di cui gli agenti hanno bisogno. Fai streaming dei token da un model provider in `metadata` mentre il run e in corso, il frontend li renderizza dal vivo e il run sopravvive a chiamate di tool a lunga durata senza bruciare 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~384Il frontend usa `useRealtimeRun` e legge `run.metadata.partial` per renderizzare la risposta in streaming, nello stesso modo in cui renderizzeresti una chat completion - tranne che questa sopravvive a un reload completo della pagina.385~386## Deploy387~388I deploy compilano i tuoi task in un bundle versionato, costruiscono un container e fanno swap atomico del traffico. I vecchi run in-flight continuano a usare la versione precedente.389~390```bash391npx trigger.dev@latest deploy --env prod392```393~394In CI tipicamente colleghi questo nello stesso workflow che spedisce la tua 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~404Per gli ambienti di preview, passa `--env preview --branch ${{ github.head_ref }}` e Trigger.dev crea un ambiente isolato per branch, rispecchiando come Vercel gestisce i preview deployment.405~406## Self-Hosting vs Cloud407~408Trigger.dev e open source sotto la licenza Apache 2.0. Puoi fare self-hosting su qualsiasi piattaforma di container (Docker Compose, Kubernetes, Fly.io) o usare il cloud gestito su trigger.dev.409~410| Aspetto | Cloud | Self-hosted |411|--------|-------|-------------|412| **Setup** | Sign up, esegui `init` | Esegui docker-compose o Helm chart |413| **Scaling** | Automatico | Tua responsabilita |414| **Pricing** | Per run + per compute | Solo costo infra |415| **Compliance** | SOC 2 | Quello che fornisce il tuo ambiente |416| **Ideale per** | La maggior parte dei team | Data residency stretta, infra custom |417~418L'SDK e la CLI sono identici tra le due modalita - cambi un flag di profilo e punti alla tua istanza.419~420## Best Practice421~422### 1. Mantieni i payload piccoli e serializzabili423~424Passa ID e riferimenti, non oggetti completi. Recupera i dati dentro al task. Questo mantiene la coda piccola, i payload economici da loggare e ti lascia cambiare la sorgente dati senza ri-triggerare.425~426### 2. Idempotency key su ogni chiamata esterna427~428Combina `idempotencyKey` sul trigger del task con le idempotency key alle API dei tuoi vendor (Stripe, OpenAI, ecc.). I retry saranno sicuri end-to-end.429~430### 3. Usa `triggerAndWait` per l'orchestrazione, non `Promise.all` di trigger431~432Un padre che chiama `triggerAndWait` compone durabilmente i task figli. Un padre che triggera e si risolve immediatamente perde l'osservabilita della catena.433~434### 4. Tagga i run435~436Aggiungi `tags` ai trigger (`tags: ["user:123", "feature:onboarding"]`) cosi puoi filtrare la dashboard e la management API per dimensioni di business.437~438### 5. Mantieni `init` idempotente439~440Gira a ogni cold start. Evita migrazioni o effetti collaterali one-shot li dentro.441~442## Conclusione443~444Trigger.dev rimuove le categorie di lavoro che richiedevano la costruzione di un sistema di job da zero. Scrivi TypeScript async, lo chiami da qualsiasi punto e la piattaforma ti da esecuzione durabile, scheduling, code, retry, aggiornamenti realtime e pattern human-in-the-loop out of the box.445~446La stessa superficie che alimenta un cron notturno e la superficie che alimenta un agente AI multi-step che fa streaming al frontend e si mette in pausa per la review. Quella convergenza e cio che rende il framework degno di uno sguardo serio nel 2026, sia che tu stia gestendo un SaaS che ha bisogno di lavoro in background affidabile sia che stia spedendo feature AI che sopravvivono a un timeout serverless.447~448> **Checklist per Iniziare:**449>450> - [x] Iscriviti su trigger.dev o esegui lo stack Docker self-hosted451> - [x] `npx trigger.dev@latest init` nel tuo progetto452> - [x] Definisci il tuo primo task con `task({ id, run })`453> - [x] Triggeralo dalla tua API e guarda il run nella dashboard454> - [x] Aggiungi `idempotencyKey` e `concurrencyKey` per la sicurezza in produzione455> - [x] Collega `useRealtimeRun` in un componente di status456> - [x] Fai il deploy con `trigger.dev deploy --env prod` da CI457~
NORMAL · trigger-dev-background-jobs-guide.md [readonly]457 lines · :q to close