La plupart des applications en production ont besoin de travail qui ne rentre pas dans le cycle requete/reponse : envoi d'emails, traitement d'uploads, execution de pipelines IA, synchronisation de donnees tierces, generation de rapports. La reponse traditionnelle est une queue (Redis, SQS, RabbitMQ), une flotte de workers, un scheduler et une pile fragile de code de glue qui casse a chaque deploiement.
Trigger.dev reduit cette stack a un seul SDK TypeScript. Vous ecrivez des fonctions, vous les appelez de n'importe ou et la plateforme gere le queueing, les retries, l'observabilite, le scheduling et l'execution durable. Les tasks tournent aussi longtemps que necessaire - pas de timeout serverless de 10 secondes, pas de travail perdu lors des redeploys.
Pourquoi Trigger.dev
Le changement en 2026 est l'execution durable. Les workflows doivent survivre aux redemarrages, crashes, deploiements et rate limits. Ils doivent aussi streamer le progres vers l'UI en temps reel et se mettre en pause pour l'input humain. Trigger.dev a ete reconstruit autour de ces exigences avec la version 3 et continue a etendre sa surface d'infrastructure pour l'IA.
Le modele est simple : vous definissez les tasks comme exports, le SDK les recupere, la plateforme les schedule et les execute dans des containers isoles et l'etat du run est persiste pour que vous puissiez reprendre, retry et observer.
Pour commencer
Initialiser un projet
npx trigger.dev@latest login npx trigger.dev@latest init
Cela cree un fichier trigger.config.ts et un repertoire trigger/ avec des tasks d'exemple. Le fichier de config est la source de verite pour votre projet : quels repertoires contiennent les tasks, parametres de build, lifecycle hooks et options de 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"], });
Executer les tasks en local
npx trigger.dev@latest dev
Le serveur de dev se connecte au cloud, enregistre vos tasks et streame les runs a travers votre code local. Vous mettez des breakpoints dans votre editeur et vous les atteignez sur de vrais triggers - la meme boucle que vous utiliseriez dans n'importe quel projet Node.js normal.
Definir une Task
Une task est un objet exporte avec un id unique et une fonction run. Le SDK inspecte les exports a travers dirs et les enregistre automatiquement.
// 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 }; }, });
Trois choses a noter :
- Pas de timeout dans le corps du run. La plateforme gere le temps d'execution via
maxDurationdans la config, pas dans le runtime. - Les throws sont des retries. Le SDK attrape les exceptions et re-execute avec un backoff exponentiel selon la policy
retry. - La valeur de retour est persistee. D'autres tasks et votre frontend peuvent lire
run.outputde n'importe ou.
Triggerer les Tasks
Vous appelez une task depuis votre backend, vos routes API ou une autre 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 - utilisez-le pour tracer ou afficher le progres
Les options debloquent beaucoup de comportement en un seul appel :
idempotencyKey- si un run avec la meme key existe deja, le SDK retourne le handle existant au lieu de dupliquer le travail.concurrencyKey- serialise les runs partageant la key pour que vous ne depassiez pas un rate limit per-tenant.queue.concurrencyLimit- cap global pour la queue a travers toutes les keys.delay- schedule le run pour un temps futur.ttl- si le run n'a pas demarre d'ici la, expirez-le automatiquement.
Batch trigger
Pour les workloads de fan-out, batchTrigger accepte jusqu'a 500 items par appel et cree un run par item.
await sendWelcomeEmail.batchTrigger( newUsers.map((u) => ({ payload: { email: u.email, name: u.name }, options: { idempotencyKey: `welcome-${u.id}` }, })) );
Tasks Schedules
Les cron jobs deviennent des declarations de premiere classe. Le schedule lui-meme est un objet separe que vous pouvez attacher a une task plusieurs fois.
// 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); }, });
Pour les schedules per-tenant - disons, un cron par client - vous les creez dynamiquement via 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 rend l'appel idempotent : reexecuter le meme code au moment du deploy n'empile pas de schedules dupliques.
Queues, Concurrence et Idempotence
Trois primitives couvrent la plupart des besoins de rate-limiting et d'ordering.
Un pattern courant : une queue par tenant avec une petite concurrence per-key pour respecter le rate limit d'un vendor, plus une idempotency key pour rendre les retries surs.
await syncShopifyOrders.trigger( { shopId }, { queue: { name: `shopify-${shopId}`, concurrencyLimit: 2 }, concurrencyKey: shopId, idempotencyKey: `sync-${shopId}-${Date.now() / 60_000 | 0}`, } );
Attentes et travail de longue duree
Les tasks peuvent se mettre en pause sans tenir une connexion ou bruler du compute. La plateforme persiste l'etat et reprend la fonction quand l'attente se termine.
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 est la feature decisive : elle triggere une task enfant et suspend le parent jusqu'a ce que l'enfant se termine. Vous composez les tasks comme des fonctions async, mais l'orchestration tourne durablement a travers des jours ou des semaines.
Human-in-the-loop avec wait.forToken
Pour les flux d'approbation et les gates IA, wait.forToken met en pause jusqu'a ce que votre application reponde avec un resultat.
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'editeur ouvre une UI, revoit le brouillon, clique sur Approve et votre backend complete le token. La task reprend la ou elle s'etait arretee - meme si des heures ou jours sont passes.
Lifecycle Hooks
Vous pouvez attacher init, onStart, onSuccess et onFailure a une task ou globalement dans trigger.config.ts. Utilisez-les pour le tracing, l'error reporting et le setup partage.
// 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 tourne une fois par container worker au boot, pas par run, donc c'est le bon endroit pour configurer les clients et pools.
Realtime dans le Frontend
Trigger.dev publie les changements d'etat de run - status, metadata, output - sur une API en streaming. Les hooks React s'abonnent a ce stream et re-rendent automatiquement.
// 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> ); }
Vous generez le public access token cote serveur, scoped a un run specifique, et l'envoyez au client. Le hook gere l'auth, la reconnexion et les mises a jour incrementielles.
Pour trigger-and-subscribe en un seul coup :
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>;
Agents IA et Streaming
Trigger.dev est devenu un runtime populaire pour les agents IA car les memes primitives - execution durable, retries, attentes, metadata en temps reel, human-in-the-loop - sont exactement ce dont les agents ont besoin. Vous streamez les tokens d'un model provider dans metadata pendant que le run a lieu, le frontend les rend en direct et le run survit aux tool calls de longue duree sans bruler 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 }; }, });
Le frontend utilise useRealtimeRun et lit run.metadata.partial pour rendre la reponse en streaming, de la meme maniere que vous rendriez une chat completion - sauf que celle-ci survit a un rechargement complet de la page.
Deploiement
Les deploys compilent vos tasks dans un bundle versionne, construisent un container et echangent atomiquement le trafic. Les anciens runs en cours continuent d'utiliser la version precedente.
npx trigger.dev@latest deploy --env prod
En CI, vous cablez typiquement cela dans le meme workflow qui livre votre 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
Pour les environnements de preview, passez --env preview --branch ${{ github.head_ref }} et Trigger.dev cree un environnement isole par branche, refletant la facon dont Vercel gere les preview deployments.
Self-Hosting vs Cloud
Trigger.dev est open source sous la licence Apache 2.0. Vous pouvez self-hoster sur n'importe quelle plateforme de containers (Docker Compose, Kubernetes, Fly.io) ou utiliser le cloud manage sur trigger.dev.
| Aspect | Cloud | Self-hosted |
|---|---|---|
| Setup | Inscription, executer init | Executer docker-compose ou Helm chart |
| Scaling | Automatique | Votre responsabilite |
| Pricing | Par run + par compute | Cout d'infra uniquement |
| Compliance | SOC 2 | Ce que fournit votre environnement |
| Ideal pour | La plupart des equipes | Residence des donnees stricte, infra custom |
Le SDK et la CLI sont identiques entre les modes - vous changez un flag de profil et pointez vers votre propre instance.
Best Practices
1. Gardez les payloads petits et serialisables
Passez des IDs et des references, pas des objets complets. Recuperez les donnees a l'interieur de la task. Cela garde la queue petite, les payloads peu couteux a logger et vous laisse changer la source de donnees sans re-triggerer.
2. Idempotency keys sur chaque appel externe
Combinez idempotencyKey sur le trigger de la task avec les idempotency keys aux APIs de vos vendors (Stripe, OpenAI, etc.). Les retries seront surs end-to-end.
3. Utilisez triggerAndWait pour l'orchestration, pas Promise.all de triggers
Un parent qui appelle triggerAndWait compose durablement les tasks enfants. Un parent qui triggere et resout immediatement perd l'observabilite de la chaine.
4. Taggez les runs
Ajoutez des tags aux triggers (tags: ["user:123", "feature:onboarding"]) pour pouvoir filtrer le dashboard et la management API par dimensions metier.
5. Gardez init idempotent
Il tourne a chaque cold start. Evitez les migrations ou les effets secondaires one-shot la-dedans.
Conclusion
Trigger.dev supprime les categories de travail qui necessitaient autrefois de construire un systeme de jobs from scratch. Vous ecrivez du TypeScript async, vous l'appelez de n'importe ou et la plateforme vous donne l'execution durable, le scheduling, les queues, les retries, les mises a jour realtime et les patterns human-in-the-loop out of the box.
La meme surface qui alimente un cron nocturne est la surface qui alimente un agent IA multi-etapes qui streame vers le frontend et se met en pause pour la review. Cette convergence est ce qui rend le framework digne d'un regard serieux en 2026, que vous gerez un SaaS qui a besoin de travail en arriere-plan fiable ou que vous livrez des features IA qui survivent a un timeout serverless.
Checklist pour commencer :
- Inscription sur trigger.dev ou execution de la stack Docker self-hosted
npx trigger.dev@latest initdans votre projet- Definir votre premiere task avec
task({ id, run })- La triggerer depuis votre API et regarder le run dans le dashboard
- Ajouter
idempotencyKeyetconcurrencyKeypour la securite en production- Cabler
useRealtimeRundans un composant de status- Deployer avec
trigger.dev deploy --env proddepuis CI