De meeste productie-applicaties hebben werk nodig dat niet in de request/response-cyclus past: e-mails verzenden, uploads verwerken, AI-pipelines draaien, third-party data synchroniseren, rapporten genereren. Het traditionele antwoord is een queue (Redis, SQS, RabbitMQ), een worker fleet, een scheduler en een fragiele stapel glue code die bij elke deploy breekt.
Trigger.dev vouwt die stack samen tot een enkele TypeScript SDK. Je schrijft functies, roept ze overal vandaan aan en het platform regelt queueing, retries, observability, scheduling en duurzame uitvoering. Tasks draaien zo lang als nodig - geen 10-seconden serverless-timeout, geen verloren werk bij redeploys.
Waarom Trigger.dev
De verschuiving in 2026 is duurzame uitvoering. Workflows moeten herstarts, crashes, deploys en rate limits overleven. Ze moeten ook voortgang real-time naar de UI streamen en pauzeren voor menselijke input. Trigger.dev werd herbouwd rond deze vereisten met versie 3 en blijft zijn AI-infrastructuuroppervlak uitbreiden.
Het model is eenvoudig: je definieert tasks als exports, de SDK pikt ze op, het platform plant en draait ze in geisoleerde containers en de run-state wordt bewaard, zodat je kunt hervatten, opnieuw proberen en observeren.
Aan de slag
Initialiseer een project
npx trigger.dev@latest login npx trigger.dev@latest init
Dit creeert een trigger.config.ts bestand en een trigger/ directory met voorbeeldtasks. Het configbestand is de waarheidsbron voor je project: welke directories tasks bevatten, build-instellingen, lifecycle hooks en runtime-opties.
// 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"], });
Voer tasks lokaal uit
npx trigger.dev@latest dev
De dev-server verbindt met de cloud, registreert je tasks en streamt runs door je lokale code. Je zet breakpoints in je editor en raakt ze aan op echte triggers - dezelfde loop die je in elk normaal Node.js-project zou gebruiken.
Een Task definieren
Een task is een geexporteerd object met een uniek id en een run functie. De SDK inspecteert exports door dirs heen en registreert ze automatisch.
// 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 }; }, });
Drie dingen om op te merken:
- Geen timeout in de run-body. Het platform beheert de uitvoeringstijd via
maxDurationin de config, niet in de runtime. - Throws zijn retries. De SDK vangt exceptions en draait opnieuw met exponentiele backoff volgens het
retry-beleid. - De return-waarde wordt bewaard. Andere tasks en je frontend kunnen
run.outputoveral lezen.
Tasks triggeren
Je roept een task aan vanuit je backend, je API-routes of een andere 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 - gebruik dit om voortgang te tracken of weer te geven
De opties ontgrendelen veel gedrag in een enkele aanroep:
idempotencyKey- als een run met dezelfde sleutel al bestaat, retourneert de SDK de bestaande handle in plaats van werk te dupliceren.concurrencyKey- serialiseert runs die de sleutel delen, zodat je een per-tenant rate limit niet overschrijdt.queue.concurrencyLimit- globale cap voor de queue over alle sleutels heen.delay- plant de run voor een toekomstige tijd.ttl- als de run tegen die tijd niet is gestart, automatisch verlopen.
Batch trigger
Voor fan-out workloads accepteert batchTrigger tot 500 items per aanroep en creeert een run per item.
await sendWelcomeEmail.batchTrigger( newUsers.map((u) => ({ payload: { email: u.email, name: u.name }, options: { idempotencyKey: `welcome-${u.id}` }, })) );
Geplande Tasks
Cron jobs worden eersterangs declaraties. De schedule zelf is een apart object dat je meerdere keren aan een task kunt koppelen.
// 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); }, });
Voor per-tenant schedules - zeg, een cron per klant - maak je ze dynamisch via de 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}`, });
De deduplicationKey maakt de aanroep idempotent: dezelfde code opnieuw uitvoeren bij deploy-tijd stapelt geen dubbele schedules op.
Queues, Concurrency en Idempotency
Drie primitieven dekken de meeste behoeften aan rate-limiting en ordering.
Een veelvoorkomend patroon: een queue per tenant met een kleine per-key concurrency om de rate limit van een vendor te respecteren, plus een idempotency key om retries veilig te maken.
await syncShopifyOrders.trigger( { shopId }, { queue: { name: `shopify-${shopId}`, concurrencyLimit: 2 }, concurrencyKey: shopId, idempotencyKey: `sync-${shopId}-${Date.now() / 60_000 | 0}`, } );
Waits en langlopend werk
Tasks kunnen pauzeren zonder een verbinding vast te houden of compute te verbranden. Het platform bewaart de state en hervat de functie wanneer het wachten voltooid is.
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 is de killer feature: het triggert een child task en suspendeert de parent tot de child klaar is. Je componeert tasks als async-functies, maar de orchestratie loopt duurzaam over dagen of weken.
Human-in-the-loop met wait.forToken
Voor approval-flows en AI-gates pauzeert wait.forToken tot je applicatie terugbelt met een resultaat.
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); }, });
De editor opent een UI, beoordeelt het concept, klikt op Goedkeuren en je backend voltooit het token. De task pikt op waar hij gebleven was - zelfs als er uren of dagen zijn verstreken.
Lifecycle Hooks
Je kunt init, onStart, onSuccess en onFailure aan een task of globaal in trigger.config.ts koppelen. Gebruik ze voor tracing, error reporting en gedeelde setup.
// 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 draait een keer per worker container bij boot, niet per run, dus het is de juiste plek om clients en pools op te zetten.
Realtime in de Frontend
Trigger.dev publiceert run-state-veranderingen - status, metadata, output - over een streaming API. De React-hooks abonneren zich op die stream en re-renderen automatisch.
// 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> ); }
Je genereert het public access token aan de serverkant, scoped op een specifieke run, en stuurt het naar de client. De hook regelt auth, reconnection en incrementele updates.
Voor trigger-and-subscribe in een keer:
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>;
AI-agents en Streaming
Trigger.dev is een populaire runtime voor AI-agents geworden omdat dezelfde primitieven - duurzame uitvoering, retries, waits, real-time metadata, human-in-the-loop - precies zijn wat agents nodig hebben. Je streamt tokens van een model provider naar metadata terwijl de run plaatsvindt, de frontend rendert ze live en de run overleeft langlopende tool calls zonder een serverless-timeout te verbranden.
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 }; }, });
De frontend gebruikt useRealtimeRun en leest run.metadata.partial om de streaming-respons te renderen, op dezelfde manier waarop je een chat completion zou renderen - behalve dat deze een volledige page reload overleeft.
Deployen
Deploys compileren je tasks tot een versie-gemarkeerde bundle, bouwen een container en wisselen verkeer atomair. Oude in-flight runs blijven de vorige versie gebruiken.
npx trigger.dev@latest deploy --env prod
In CI verbind je dit doorgaans aan dezelfde workflow die je app verzendt:
# .github/workflows/deploy.yml - name: Deploy Trigger.dev env: TRIGGER_ACCESS_TOKEN: ${{ secrets.TRIGGER_ACCESS_TOKEN }} run: npx trigger.dev@latest deploy --env prod
Voor preview-omgevingen geef je --env preview --branch ${{ github.head_ref }} door en Trigger.dev maakt een geisoleerde omgeving per branch, wat de manier weerspiegelt waarop Vercel preview-deployments behandelt.
Self-Hosting vs Cloud
Trigger.dev is open source onder de Apache 2.0-licentie. Je kunt self-hosten op elke containerplatform (Docker Compose, Kubernetes, Fly.io) of de beheerde cloud op trigger.dev gebruiken.
| Aspect | Cloud | Self-hosted |
|---|---|---|
| Setup | Aanmelden, init uitvoeren | Docker-compose of Helm chart uitvoeren |
| Scaling | Automatisch | Jouw verantwoordelijkheid |
| Pricing | Per run + per compute | Alleen infra-kosten |
| Compliance | SOC 2 | Wat je omgeving biedt |
| Beste voor | De meeste teams | Strikte data residency, custom infra |
De SDK en CLI zijn identiek tussen modi - je verandert een profile flag en wijst naar je eigen instance.
Best Practices
1. Houd payloads klein en serializeerbaar
Geef IDs en referenties door, geen volledige objecten. Haal data op binnen de task. Dit houdt de queue klein, payloads goedkoop om te loggen en laat je de databron veranderen zonder opnieuw te triggeren.
2. Idempotency keys op elke externe call
Combineer idempotencyKey op de task trigger met idempotency keys op je vendor-API's (Stripe, OpenAI, etc.). Retries zijn end-to-end veilig.
3. Gebruik triggerAndWait voor orchestratie, niet Promise.all van triggers
Een parent die triggerAndWait aanroept, componeert duurzaam child tasks. Een parent die triggert en onmiddellijk resolvet, verliest de observability van de keten.
4. Tag runs
Voeg tags toe aan triggers (tags: ["user:123", "feature:onboarding"]) zodat je het dashboard en de management API kunt filteren op zakelijke dimensies.
5. Houd init idempotent
Het draait bij elke cold start. Vermijd migraties of one-shot bijwerkingen daar.
Conclusie
Trigger.dev verwijdert de werkcategorieen die vroeger het bouwen van een job-systeem vanaf nul vereisten. Je schrijft async TypeScript, je roept het overal vandaan aan en het platform geeft je duurzame uitvoering, scheduling, queues, retries, real-time updates en human-in-the-loop patronen out of the box.
Hetzelfde oppervlak dat een nachtelijke cron aandrijft, is het oppervlak dat een multi-step AI-agent aandrijft die naar de frontend streamt en pauzeert voor review. Die convergentie is wat het framework de moeite waard maakt voor een serieuze blik in 2026, of je nu een SaaS draait die betrouwbaar achtergrondwerk nodig heeft of AI-features verstuurt die een serverless-timeout overleven.
Aan de slag Checklist:
- Meld je aan op trigger.dev of draai de self-hosted Docker-stack
npx trigger.dev@latest initin je project- Definieer je eerste task met
task({ id, run })- Trigger het vanuit je API en bekijk de run in het dashboard
- Voeg
idempotencyKeyenconcurrencyKeytoe voor productieveiligheid- Verbind
useRealtimeRunmet een statuscomponent- Deploy met
trigger.dev deploy --env prodvanuit CI