Pourquoi Trigger.dev v4 ?
Trigger.dev v3 a atteint sa fin de vie le 1er juillet 2026. Si votre projet importe encore depuis @trigger.dev/sdk/v3, vos tâches en arrière-plan ne s'exécutent plus — l'infrastructure cloud v3 est entièrement arrêtée.
La v4 n'est pas une mise à jour cosmétique. Elle réécrit l'API des hooks, introduit un système de middleware, ajoute des flux d'approbation wait-for-token, et propose schemaTask — un pont qui transforme n'importe quelle tâche en arrière-plan en outil de première classe pour le Vercel AI SDK. Ce tutoriel vous guide à travers la migration et construit un pipeline complet de traitement de documents illustrant chaque fonctionnalité majeure de la v4.
Prérequis
- Node.js 20+
- Un projet Next.js 15 (App Router)
- Un compte Trigger.dev — gratuit sur trigger.dev
- Familiarité de base avec TypeScript asynchrone
Ce que vous allez construire
Un pipeline d'ingestion de documents en quatre étapes :
- Ingestion — accepter un upload et mettre le traitement en file d'attente
- Analyse — extraction de contenu par IA avec contrôle de concurrence
- Approbation — mise en pause et attente d'un relecteur humain avant publication
- Notification — reprise et envoi d'une confirmation à l'approbation
Étape 1 : Installer le SDK v4
npm install @trigger.dev/sdk
npx trigger.dev@latest initLa commande init crée un fichier trigger.config.ts à la racine du projet et un répertoire trigger/ pour les fichiers de tâches.
Si vous migrez depuis la v3, le premier changement est le chemin d'import :
// Avant (v3)
import { task } from "@trigger.dev/sdk/v3";
// Après (v4)
import { task } from "@trigger.dev/sdk";Configurez votre projet dans trigger.config.ts :
// trigger.config.ts
import { defineConfig } from "@trigger.dev/sdk/config";
export default defineConfig({
project: "proj_VOTRE_ID_PROJET",
runtime: "node",
maxDuration: 3600,
dirs: ["./trigger"],
});Ajoutez votre clé API dans .env.local :
TRIGGER_SECRET_KEY=tr_dev_xxxxxxxxxxxxÉtape 2 : Votre première tâche v4
Le changement majeur de la v4 concerne l'API des paramètres de hook. En v3, les hooks recevaient des arguments positionnels séparés. En v4, chaque hook reçoit un unique objet déstructuré.
// trigger/document-tasks.ts
import { task, logger } from "@trigger.dev/sdk";
export const ingestDocument = task({
id: "ingest-document",
// v4 : paramètre unique déstructuré pour tous les hooks
onStartAttempt: ({ payload, ctx }) => {
logger.info("Démarrage de la tentative", { runId: ctx.run.id });
},
onSuccess: ({ payload, output, ctx }) => {
logger.info("Ingestion réussie", { docId: output.documentId });
},
onFailure: ({ payload, error, ctx }) => {
logger.error("Échec de l'ingestion", { message: error.message });
},
// La signature de run() est inchangée par rapport à la v3
run: async (payload: { filename: string; url: string }, { ctx }) => {
logger.info("Traitement du document", { filename: payload.filename });
await new Promise(resolve => setTimeout(resolve, 1000));
return {
documentId: `doc_${Date.now()}`,
filename: payload.filename,
status: "ingested",
};
},
});Remarque :
onStartAttemptest le successeur v4 du hookonStart(désormais déprécié). UtilisezonStartAttemptpour tout nouveau code.
Étape 3 : Déclencher depuis Next.js
Server Action
// app/actions/document.ts
"use server";
import { tasks } from "@trigger.dev/sdk";
import type { ingestDocument } from "@/trigger/document-tasks";
export async function submitDocument(filename: string, url: string) {
try {
const handle = await tasks.trigger<typeof ingestDocument>(
"ingest-document",
{ filename, url }
);
return { runId: handle.id };
} catch (error) {
return { error: "Échec de la mise en file d'attente" };
}
}Route API
// app/api/documents/route.ts
import { tasks } from "@trigger.dev/sdk";
import { NextResponse } from "next/server";
import type { ingestDocument } from "@/trigger/document-tasks";
export async function POST(request: Request) {
const { filename, url } = await request.json();
const handle = await tasks.trigger<typeof ingestDocument>(
"ingest-document",
{ filename, url }
);
return NextResponse.json({ runId: handle.id });
}L'import type-uniquement garantit que le code de la tâche ne se retrouve jamais dans le bundle client.
Étape 4 : Files d'attente et concurrence
Utilisez queue() pour contrôler le nombre d'exécutions simultanées — essentiel pour protéger votre base de données ou vos APIs tierces.
// trigger/analysis-tasks.ts
import { task, queue } from "@trigger.dev/sdk";
const processingQueue = queue({
name: "document-processing",
concurrencyLimit: 5,
});
export const analyzeDocument = task({
id: "analyze-document",
queue: processingQueue,
retry: {
maxAttempts: 3,
minTimeoutInMs: 1000,
maxTimeoutInMs: 30000,
factor: 2,
},
run: async (payload: { documentId: string; url: string }) => {
const text = await fetch(payload.url).then(r => r.text());
const wordCount = text.split(/\s+/).length;
return {
documentId: payload.documentId,
wordCount,
extractedAt: new Date().toISOString(),
};
},
});Vous pouvez aussi surcharger la file au moment du déclenchement pour le routage prioritaire :
// Router les utilisateurs premium vers une file à plus haute concurrence
const handle = await analyzeDocument.trigger(
{ documentId, url },
{ queue: "premium-users" }
);Étape 5 : Wait-for-Token — Flux d'approbation humaine
La nouvelle primitive la plus puissante de la v4 est le jeton de point d'attente (waitpoint token). Une tâche se met en pause en cours d'exécution et ne reprend que lorsqu'un système externe complète ce jeton — quelques minutes, heures, voire jours plus tard — sans occuper un thread serveur.
// trigger/approval-tasks.ts
import { task, wait } from "@trigger.dev/sdk";
export const awaitDocumentApproval = task({
id: "await-document-approval",
maxDuration: 86400, // mise en pause jusqu'à 24 heures
run: async (payload: { documentId: string; reviewerEmail: string }) => {
// Créer un jeton lié à cette exécution
const token = await wait.createToken({
timeout: "24h",
tags: [`doc:${payload.documentId}`],
});
// Envoyer un email contenant un lien avec token.id
await notifyReviewer(payload.reviewerEmail, payload.documentId, token.id);
// La tâche se met en pause ici — reprend à la complétion du jeton
const result = await wait.forToken<{ approved: boolean; notes: string }>(token);
if (!result.output.approved) {
throw new Error(`Document rejeté : ${result.output.notes}`);
}
return { approved: true, notes: result.output.notes };
},
});
async function notifyReviewer(email: string, docId: string, tokenId: string) {
// Envoyer email avec lien : /api/review?token=tokenId&doc=docId
console.log(`Lien d'approbation envoyé à ${email} pour le document ${docId}`);
}Compléter le jeton depuis une route API
// app/api/review/route.ts
import { runs } from "@trigger.dev/sdk";
import { NextResponse } from "next/server";
export async function POST(request: Request) {
const { tokenId, approved, notes } = await request.json();
// Compléter le point d'attente — la tâche reprend immédiatement
await runs.completeToken(tokenId, { approved, notes });
return NextResponse.json({ ok: true });
}La primitive wait.forToken gère n'importe quel handoff asynchrone qui nécessiterait autrement du polling ou des webhooks.
Étape 6 : schemaTask comme outil IA
La v4 introduit schemaTask — une variante de tâche avec un schéma Zod déclaré. Cela active ai.toolExecute, qui convertit n'importe quelle tâche en arrière-plan en outil appelable par des modèles de langage via le Vercel AI SDK.
// trigger/ai-tasks.ts
import { schemaTask } from "@trigger.dev/sdk";
import { z } from "zod";
export const summarizeDocument = schemaTask({
id: "summarize-document",
description: "Extraire un résumé et les sujets clés d'une URL de document",
schema: z.object({
documentId: z.string(),
url: z.string().url(),
maxWords: z.number().int().min(50).max(500).default(200),
}),
run: async ({ documentId, url, maxWords }) => {
const text = await fetch(url).then(r => r.text());
const words = text.split(/\s+/).slice(0, maxWords);
return {
documentId,
summary: words.join(" "),
topicCount: Math.min(3, Math.floor(words.length / 50)),
};
},
});Connecter comme outil Vercel AI SDK
// lib/document-agent.ts
import { generateText, tool } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { ai } from "@trigger.dev/sdk/ai";
import { summarizeDocument } from "@/trigger/ai-tasks";
const summarizeTool = tool({
description: summarizeDocument.description ?? "Résumer un document",
inputSchema: summarizeDocument.schema!,
execute: ai.toolExecute(summarizeDocument),
});
export async function runDocumentAgent(userPrompt: string) {
const { text } = await generateText({
model: anthropic("claude-sonnet-5"),
prompt: userPrompt,
tools: { summarizeDocument: summarizeTool },
maxSteps: 5,
});
return text;
}Lorsque le modèle invoque summarizeDocument, la tâche Trigger.dev s'exécute en arrière-plan — avec retries, contrôle de concurrence et monitoring en temps réel — plutôt qu'inline dans votre route API.
Étape 7 : Tâches planifiées (Cron)
// trigger/scheduled-tasks.ts
import { schedules } from "@trigger.dev/sdk";
export const dailyCleanup = schedules.task({
id: "daily-cleanup",
cron: "0 2 * * *", // 02:00 UTC chaque jour
run: async (payload) => {
const { timestamp, lastTimestamp } = payload;
const cutoff = new Date(timestamp);
cutoff.setDate(cutoff.getDate() - 30);
// payload.lastTimestamp est undefined lors de la première exécution
const since = lastTimestamp ?? cutoff;
const deleted = await deleteDocumentsOlderThan(cutoff);
return { deleted, runAt: timestamp.toISOString(), since: since.toISOString() };
},
});
async function deleteDocumentsOlderThan(cutoff: Date): Promise<number> {
// Votre requête de base de données ici
return 0;
}Aucune configuration supplémentaire n'est nécessaire — le champ cron enregistre automatiquement la planification au déploiement.
Étape 8 : Nouveaux hooks de cycle de vie v4
La v4 ajoute des hooks qui se déclenchent autour des points d'attente, offrant une observabilité sur les exécutions mises en pause :
// trigger/monitored-task.ts
import { task, logger } from "@trigger.dev/sdk";
export const monitoredTask = task({
id: "monitored-task",
// Se déclenche lorsque l'exécution entre dans un point d'attente
onWait: ({ payload, ctx }) => {
logger.info("Tâche mise en pause sur un point d'attente", { runId: ctx.run.id });
},
// Se déclenche lorsque le point d'attente se résout et que l'exécution reprend
onResume: ({ payload, ctx }) => {
logger.info("Tâche reprise depuis un point d'attente", { runId: ctx.run.id });
},
// Se déclenche après le retour réussi de la fonction run
onComplete: ({ payload, output, ctx }) => {
logger.info("Tâche terminée", { runId: ctx.run.id, output });
},
// Se déclenche lors de l'annulation via le tableau de bord ou l'API
onCancel: ({ payload, ctx }) => {
logger.warn("Tâche annulée", { runId: ctx.run.id });
},
run: async (payload: { documentId: string }) => {
return { processed: payload.documentId };
},
});Ces hooks complètent les hooks existants onSuccess, onFailure et onStartAttempt, et sont utiles pour émettre des métriques vers Datadog, PostHog ou tout autre système d'observabilité.
Étape 9 : Déclenchement par lot (changement API v4)
L'API de lot a changé en v4. Vous récupérez maintenant les résultats avec batch.retrieve() au lieu d'accéder directement à batchHandle.runs :
import { tasks, batch } from "@trigger.dev/sdk";
import type { ingestDocument } from "@/trigger/document-tasks";
const documents = [
{ filename: "rapport-a.pdf", url: "https://exemple.com/a.pdf" },
{ filename: "rapport-b.pdf", url: "https://exemple.com/b.pdf" },
{ filename: "rapport-c.pdf", url: "https://exemple.com/c.pdf" },
];
const batchHandle = await tasks.batchTrigger(
documents.map(doc => [ingestDocument, doc] as const)
);
// v3 : batchHandle.runs — SUPPRIMÉ en v4
// v4 : récupération séparée
const batchResult = await batch.retrieve(batchHandle.batchId);
console.log(`${batchResult.runs.length} exécutions mises en file d'attente`);Étape 10 : Développement et déploiement
Développement local
npx trigger.dev@latest devCette commande diffuse les logs des tâches dans votre terminal et recharge à chaud les fichiers de tâches à la sauvegarde. Votre serveur Next.js dev tourne séparément.
Déployer sur le cloud Trigger.dev
npx trigger.dev@latest deployGitHub Actions CI
- name: Déployer les tâches Trigger.dev
run: npx trigger.dev@latest deploy --ci
env:
TRIGGER_SECRET_KEY: ${{ secrets.TRIGGER_SECRET_KEY }}Référence de migration : v3 vers v4
| Ce qui a changé | v3 | v4 |
|---|---|---|
| Chemin d'import | @trigger.dev/sdk/v3 | @trigger.dev/sdk |
| Paramètres de hook | arguments positionnels | objet unique déstructuré |
| Nom du hook | onStart | onStartAttempt (privilégié) |
| Résultats de lot | batchHandle.runs | batch.retrieve(id).runs |
| Flux d'approbation | non disponible | wait.createToken + wait.forToken |
| Pont outil IA | non disponible | schemaTask + ai.toolExecute |
| Hooks pause/reprise | non disponibles | onWait, onResume, onComplete, onCancel |
Résolution de problèmes
La tâche ne s'exécute pas : vérifiez que TRIGGER_SECRET_KEY correspond à votre projet. Exécutez npx trigger.dev@latest whoami pour vérifier la connexion.
Exécution bloquée en état "waiting" : un jeton de point d'attente a été créé mais jamais complété. Appelez runs.completeToken depuis votre endpoint d'approbation, ou annulez l'exécution depuis le tableau de bord Trigger.dev.
Erreurs TypeScript sur les paramètres de hook : vous utilisez encore des arguments positionnels de style v3. Regroupez-les dans un unique objet déstructuré comme montré à l'Étape 2.
batchHandle.runs est undefined : vous utilisez l'ancienne API de lot v3. Migrez vers batch.retrieve(batchHandle.batchId) comme montré à l'Étape 9.
Prochaines étapes
- Recherche vectorielle LanceDB dans Next.js — stocker et interroger les embeddings extraits par IA
- AI SDK 7 HarnessAgent — orchestrer des outils
schemaTaskTrigger.dev avecHarnessAgent - Passerelle LiteLLM Proxy — router les appels IA via une passerelle auto-hébergée avec budgets par équipe
Conclusion
Trigger.dev v4 apporte des améliorations significatives par rapport à la v3 : une API de hooks plus propre, wait-for-token pour les flux humain-dans-la-boucle asynchrones, schemaTask pour l'intégration des outils IA, et de nouveaux hooks de cycle de vie pour l'observabilité autour des points d'attente. Avec la v3 entièrement arrêtée depuis juillet 2026, la migration n'est pas optionnelle — mais la mise à niveau est directe, et les nouvelles fonctionnalités justifient l'effort.