spinny:~/writing $ vim trigger-dev-background-jobs-guide.md
1~2La 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.3~4[Trigger.dev](https://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.5~6## Pourquoi Trigger.dev7~8Le 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.9~10```mermaid11graph LR12 App[Votre App] -->|trigger| API[API Trigger.dev]13 API --> Queue[Queue Durable]14 Queue --> Worker[Container Worker]15 Worker -->|run task| Task[Code de votre Task]16 Task -->|metadata| Realtime[Stream Realtime]17 Realtime --> UI[UI React]18 Worker --> Storage[Store Etat Run]19```20~21Le 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.22~23## Pour commencer24~25### Initialiser un projet26~27```bash28npx trigger.dev@latest login29npx trigger.dev@latest init30```31~32Cela 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.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### Executer les tasks en local57~58```bash59npx trigger.dev@latest dev60```61~62Le 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.63~64## Definir une Task65~66Une 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.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~97Trois choses a noter :98~991. **Pas de timeout dans le corps du run.** La plateforme gere le temps d'execution via `maxDuration` dans la config, pas dans le runtime.1002. **Les throws sont des retries.** Le SDK attrape les exceptions et re-execute avec un backoff exponentiel selon la policy `retry`.1013. **La valeur de retour est persistee.** D'autres tasks et votre frontend peuvent lire `run.output` de n'importe ou.102~103## Triggerer les Tasks104~105Vous appelez une task depuis votre backend, vos routes API ou une autre 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 - utilisez-le pour tracer ou afficher le progres122```123~124Les options debloquent beaucoup de comportement en un seul appel :125~126- **`idempotencyKey`** - si un run avec la meme key existe deja, le SDK retourne le handle existant au lieu de dupliquer le travail.127- **`concurrencyKey`** - serialise les runs partageant la key pour que vous ne depassiez pas un rate limit per-tenant.128- **`queue.concurrencyLimit`** - cap global pour la queue a travers toutes les keys.129- **`delay`** - schedule le run pour un temps futur.130- **`ttl`** - si le run n'a pas demarre d'ici la, expirez-le automatiquement.131~132### Batch trigger133~134Pour les workloads de fan-out, `batchTrigger` accepte jusqu'a 500 items par appel et cree un run par 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 Schedules146~147Les 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.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~167Pour les schedules per-tenant - disons, un cron par client - vous les creez dynamiquement via 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` rend l'appel idempotent : reexecuter le meme code au moment du deploy n'empile pas de schedules dupliques.182~183## Queues, Concurrence et Idempotence184~185Trois primitives couvrent la plupart des besoins de rate-limiting et d'ordering.186~187```mermaid188graph TB189 Trigger[trigger payload] --> IK{idempotencyKey<br/>vue?}190 IK -->|oui| Reuse[Retourner run existant]191 IK -->|non| CK[bucket concurrencyKey]192 CK --> Q[Queue avec<br/>concurrencyLimit]193 Q -->|slot disponible| Run[Executer task]194 Q -->|slots pleins| Wait[Attendre dans la queue]195```196~197Un 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.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## Attentes et travail de longue duree211~212Les 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.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` 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.230~231### Human-in-the-loop avec `wait.forToken`232~233Pour les flux d'approbation et les gates IA, `wait.forToken` met en pause jusqu'a ce que votre application reponde avec un resultat.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'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.259~260## Lifecycle Hooks261~262Vous 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.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` tourne une fois par container worker au boot, pas par run, donc c'est le bon endroit pour configurer les clients et pools.280~281## Realtime dans le Frontend282~283Trigger.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.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~336Vous 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.337~338Pour trigger-and-subscribe en un seul coup :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## Agents IA et Streaming354~355Trigger.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.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~384Le 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.385~386## Deploiement387~388Les 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.389~390```bash391npx trigger.dev@latest deploy --env prod392```393~394En CI, vous cablez typiquement cela dans le meme workflow qui livre votre 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~404Pour 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.405~406## Self-Hosting vs Cloud407~408Trigger.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.409~410| Aspect | Cloud | Self-hosted |411|--------|-------|-------------|412| **Setup** | Inscription, executer `init` | Executer docker-compose ou Helm chart |413| **Scaling** | Automatique | Votre responsabilite |414| **Pricing** | Par run + par compute | Cout d'infra uniquement |415| **Compliance** | SOC 2 | Ce que fournit votre environnement |416| **Ideal pour** | La plupart des equipes | Residence des donnees stricte, infra custom |417~418Le SDK et la CLI sont identiques entre les modes - vous changez un flag de profil et pointez vers votre propre instance.419~420## Best Practices421~422### 1. Gardez les payloads petits et serialisables423~424Passez 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.425~426### 2. Idempotency keys sur chaque appel externe427~428Combinez `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.429~430### 3. Utilisez `triggerAndWait` pour l'orchestration, pas `Promise.all` de triggers431~432Un parent qui appelle `triggerAndWait` compose durablement les tasks enfants. Un parent qui triggere et resout immediatement perd l'observabilite de la chaine.433~434### 4. Taggez les runs435~436Ajoutez des `tags` aux triggers (`tags: ["user:123", "feature:onboarding"]`) pour pouvoir filtrer le dashboard et la management API par dimensions metier.437~438### 5. Gardez `init` idempotent439~440Il tourne a chaque cold start. Evitez les migrations ou les effets secondaires one-shot la-dedans.441~442## Conclusion443~444Trigger.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.445~446La 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.447~448> **Checklist pour commencer :**449>450> - [x] Inscription sur trigger.dev ou execution de la stack Docker self-hosted451> - [x] `npx trigger.dev@latest init` dans votre projet452> - [x] Definir votre premiere task avec `task({ id, run })`453> - [x] La triggerer depuis votre API et regarder le run dans le dashboard454> - [x] Ajouter `idempotencyKey` et `concurrencyKey` pour la securite en production455> - [x] Cabler `useRealtimeRun` dans un composant de status456> - [x] Deployer avec `trigger.dev deploy --env prod` depuis CI457~
NORMAL · trigger-dev-background-jobs-guide.md [readonly]457 lines · :q to close