La 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.
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.
Perche Trigger.dev
Lo 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.
Il 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.
Per Iniziare
Inizializza un progetto
npx trigger.dev@latest login npx trigger.dev@latest init
Questo 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.
// trigger.config.ts import { defineConfig } from "@trigger.dev/sdk"; export default defineConfig({ project: "proj_abc123", runtime: "node", logLevel: "log", maxDuration: 3600, retries: { enabledInDev: true, default: { maxAttempts: 3, factor: 2, minTimeoutInMs: 1000, maxTimeoutInMs: 30_000, }, }, dirs: ["./trigger"], });
Esegui i task in locale
npx trigger.dev@latest dev
Il 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.
Definire un Task
Un task e un oggetto esportato con un id unico e una funzione run. L'SDK ispeziona gli export attraverso dirs e li registra automaticamente.
// trigger/send-welcome-email.ts import { task } from "@trigger.dev/sdk"; import { Resend } from "resend"; const resend = new Resend(process.env.RESEND_API_KEY); export const sendWelcomeEmail = task({ id: "send-welcome-email", retry: { maxAttempts: 5, factor: 1.8, minTimeoutInMs: 500, maxTimeoutInMs: 30_000, }, run: async (payload: { email: string; name: string }) => { const { data, error } = await resend.emails.send({ from: "hello@spinny.dev", to: payload.email, subject: `Welcome, ${payload.name}`, html: `<p>Glad you are here, ${payload.name}.</p>`, }); if (error) throw error; return { messageId: data?.id }; }, });
Tre cose da notare:
- Nessun timeout nel corpo del run. La piattaforma gestisce il tempo di esecuzione tramite
maxDurationnel config, non nel runtime. - Le throw sono retry. L'SDK cattura le eccezioni e ri-esegue con backoff esponenziale secondo la policy
retry. - Il valore di ritorno viene persistito. Altri task e il tuo frontend possono leggere
run.outputda qualsiasi punto.
Triggerare i Task
Chiami un task dal tuo backend, dalle tue route API o da un altro task.
import { sendWelcomeEmail } from "@/trigger/send-welcome-email"; const handle = await sendWelcomeEmail.trigger( { email: "user@example.com", name: "Alex" }, { idempotencyKey: `welcome-${userId}`, concurrencyKey: `tenant-${tenantId}`, queue: { name: "emails", concurrencyLimit: 50 }, delay: "30s", ttl: "10m", } ); console.log(handle.id); // run_xyz - usalo per tracciare o mostrare il progresso
Le opzioni sbloccano molto comportamento in una singola chiamata:
idempotencyKey- se un run con la stessa key esiste gia, l'SDK restituisce l'handle esistente invece di duplicare il lavoro.concurrencyKey- serializza i run che condividono la key cosi non superi il rate limit per-tenant.queue.concurrencyLimit- cap globale per la coda attraverso tutte le key.delay- schedula il run per un tempo futuro.ttl- se il run non e partito entro quel momento, lo fa scadere automaticamente.
Batch trigger
Per workload di fan-out, batchTrigger accetta fino a 500 elementi per chiamata e crea un run per elemento.
await sendWelcomeEmail.batchTrigger( newUsers.map((u) => ({ payload: { email: u.email, name: u.name }, options: { idempotencyKey: `welcome-${u.id}` }, })) );
Task Schedulati
I cron job diventano dichiarazioni di prima classe. Lo schedule stesso e un oggetto separato che puoi attaccare a un task piu volte.
// trigger/daily-digest.ts import { schedules } from "@trigger.dev/sdk"; export const dailyDigest = schedules.task({ id: "daily-digest", cron: "0 9 * * *", run: async (payload) => { console.log("Scheduled at:", payload.timestamp); console.log("Last run:", payload.lastTimestamp); console.log("Timezone:", payload.timezone); console.log("Next 5 runs:", payload.upcoming); await sendDigestForDate(payload.timestamp); }, });
Per schedule per-tenant - diciamo, un cron per cliente - li crei dinamicamente attraverso la management API.
import { schedules } from "@trigger.dev/sdk"; await schedules.create({ task: "daily-digest", cron: "0 9 * * *", timezone: "America/New_York", externalId: `customer_${customerId}`, deduplicationKey: `digest-${customerId}`, });
La deduplicationKey rende la chiamata idempotente: rieseguire lo stesso codice al deploy time non accatasta schedule duplicati.
Code, Concorrenza e Idempotenza
Tre primitive coprono la maggior parte delle esigenze di rate-limiting e ordering.
Un 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.
await syncShopifyOrders.trigger( { shopId }, { queue: { name: `shopify-${shopId}`, concurrencyLimit: 2 }, concurrencyKey: shopId, idempotencyKey: `sync-${shopId}-${Date.now() / 60_000 | 0}`, } );
Attese e Lavoro a Lunga Durata
I 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.
import { wait } from "@trigger.dev/sdk"; export const onboarding = task({ id: "onboarding", run: async (payload: { userId: string }) => { await sendWelcomeEmail.triggerAndWait({ userId: payload.userId }); await wait.for({ days: 1 }); await sendTipsEmail.trigger({ userId: payload.userId }); await wait.until({ date: oneWeekFromSignup(payload.userId) }); await sendUpgradeOffer.trigger({ userId: payload.userId }); }, });
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.
Human-in-the-loop con wait.forToken
Per flussi di approvazione e gate AI, wait.forToken mette in pausa finche la tua applicazione non risponde con un risultato.
import { task, wait } from "@trigger.dev/sdk"; export const publishPost = task({ id: "publish-post", run: async (payload: { draftId: string }) => { const draft = await generateAIContent(payload.draftId); const token = await wait.createToken({ timeout: "7d" }); await notifyEditor({ draftId: draft.id, token: token.id }); const decision = await wait.forToken<{ approved: boolean; notes?: string }>( token.id ); if (decision.approved) { return await publish(draft); } return await applyFeedback(draft, decision.notes); }, });
L'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.
Lifecycle Hook
Puoi attaccare init, onStart, onSuccess e onFailure a un task o globalmente in trigger.config.ts. Usali per tracing, error reporting e setup condiviso.
// trigger.config.ts export default defineConfig({ // ... init: async () => { Sentry.init({ dsn: process.env.SENTRY_DSN }); }, onFailure: async ({ error, ctx }) => { Sentry.captureException(error, { tags: { taskId: ctx.task.id, runId: ctx.run.id }, }); }, });
init gira una volta per container worker al boot, non per run, quindi e il posto giusto per configurare client e pool.
Realtime nel Frontend
Trigger.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.
// trigger/process-video.ts import { task, metadata } from "@trigger.dev/sdk"; export const processVideo = task({ id: "process-video", run: async (payload: { videoId: string }) => { metadata.set("stage", "transcoding"); await transcode(payload.videoId); metadata.set("stage", "thumbnails"); await generateThumbnails(payload.videoId); metadata.set("stage", "uploading"); const url = await uploadToCDN(payload.videoId); return { url }; }, });
// components/VideoStatus.tsx "use client"; import { useRealtimeRun } from "@trigger.dev/react-hooks"; import type { processVideo } from "@/trigger/process-video"; export function VideoStatus({ runId, publicAccessToken, }: { runId: string; publicAccessToken: string; }) { const { run, error } = useRealtimeRun<typeof processVideo>(runId, { accessToken: publicAccessToken, }); if (error) return <p>Error: {error.message}</p>; if (!run) return <p>Loading...</p>; return ( <div> <p>Status: {run.status}</p> <p>Stage: {String(run.metadata?.stage ?? "queued")}</p> {run.output?.url && <video src={run.output.url} controls />} </div> ); }
Generi il public access token lato server, scoped a uno specifico run, e lo spedisci al client. L'hook gestisce auth, riconnessione e aggiornamenti incrementali.
Per trigger-and-subscribe in un colpo solo:
import { useRealtimeTaskTrigger } from "@trigger.dev/react-hooks"; const { submit, run, isLoading } = useRealtimeTaskTrigger<typeof processVideo>( "process-video", { accessToken: publicAccessToken } ); <button onClick={() => submit({ videoId })} disabled={isLoading}> Process video </button>;
Agenti AI e Streaming
Trigger.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.
import { task, metadata } from "@trigger.dev/sdk"; import { streamText } from "ai"; import { anthropic } from "@ai-sdk/anthropic"; export const researchAgent = task({ id: "research-agent", maxDuration: 1800, run: async (payload: { question: string }) => { const result = streamText({ model: anthropic("claude-opus-4-7"), system: "You are a research assistant. Use the web.", prompt: payload.question, tools: { webSearch }, }); let fullText = ""; for await (const chunk of result.textStream) { fullText += chunk; metadata.set("partial", fullText); } return { answer: fullText, usage: await result.usage }; }, });
Il 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.
Deploy
I 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.
npx trigger.dev@latest deploy --env prod
In CI tipicamente colleghi questo nello stesso workflow che spedisce la tua app:
# .github/workflows/deploy.yml - name: Deploy Trigger.dev env: TRIGGER_ACCESS_TOKEN: ${{ secrets.TRIGGER_ACCESS_TOKEN }} run: npx trigger.dev@latest deploy --env prod
Per 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.
Self-Hosting vs Cloud
Trigger.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.
| Aspetto | Cloud | Self-hosted |
|---|---|---|
| Setup | Sign up, esegui init | Esegui docker-compose o Helm chart |
| Scaling | Automatico | Tua responsabilita |
| Pricing | Per run + per compute | Solo costo infra |
| Compliance | SOC 2 | Quello che fornisce il tuo ambiente |
| Ideale per | La maggior parte dei team | Data residency stretta, infra custom |
L'SDK e la CLI sono identici tra le due modalita - cambi un flag di profilo e punti alla tua istanza.
Best Practice
1. Mantieni i payload piccoli e serializzabili
Passa 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.
2. Idempotency key su ogni chiamata esterna
Combina idempotencyKey sul trigger del task con le idempotency key alle API dei tuoi vendor (Stripe, OpenAI, ecc.). I retry saranno sicuri end-to-end.
3. Usa triggerAndWait per l'orchestrazione, non Promise.all di trigger
Un padre che chiama triggerAndWait compone durabilmente i task figli. Un padre che triggera e si risolve immediatamente perde l'osservabilita della catena.
4. Tagga i run
Aggiungi tags ai trigger (tags: ["user:123", "feature:onboarding"]) cosi puoi filtrare la dashboard e la management API per dimensioni di business.
5. Mantieni init idempotente
Gira a ogni cold start. Evita migrazioni o effetti collaterali one-shot li dentro.
Conclusione
Trigger.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.
La 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.
Checklist per Iniziare:
- Iscriviti su trigger.dev o esegui lo stack Docker self-hosted
npx trigger.dev@latest initnel tuo progetto- Definisci il tuo primo task con
task({ id, run })- Triggeralo dalla tua API e guarda il run nella dashboard
- Aggiungi
idempotencyKeyeconcurrencyKeyper la sicurezza in produzione- Collega
useRealtimeRunin un componente di status- Fai il deploy con
trigger.dev deploy --env prodda CI