spinny:~/writing $ vim trigger-dev-background-jobs-guide.md
1~2De 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.3~4[Trigger.dev](https://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.5~6## Waarom Trigger.dev7~8De 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.9~10```mermaid11graph LR12 App[Jouw App] -->|trigger| API[Trigger.dev API]13 API --> Queue[Duurzame Queue]14 Queue --> Worker[Worker Container]15 Worker -->|run task| Task[Jouw Task code]16 Task -->|metadata| Realtime[Realtime Stream]17 Realtime --> UI[React UI]18 Worker --> Storage[Run State Store]19```20~21Het 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.22~23## Aan de slag24~25### Initialiseer een project26~27```bash28npx trigger.dev@latest login29npx trigger.dev@latest init30```31~32Dit 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.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### Voer tasks lokaal uit57~58```bash59npx trigger.dev@latest dev60```61~62De 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.63~64## Een Task definieren65~66Een task is een geexporteerd object met een uniek `id` en een `run` functie. De SDK inspecteert exports door `dirs` heen en registreert ze automatisch.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~97Drie dingen om op te merken:98~991. **Geen timeout in de run-body.** Het platform beheert de uitvoeringstijd via `maxDuration` in de config, niet in de runtime.1002. **Throws zijn retries.** De SDK vangt exceptions en draait opnieuw met exponentiele backoff volgens het `retry`-beleid.1013. **De return-waarde wordt bewaard.** Andere tasks en je frontend kunnen `run.output` overal lezen.102~103## Tasks triggeren104~105Je roept een task aan vanuit je backend, je API-routes of een andere 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 - gebruik dit om voortgang te tracken of weer te geven122```123~124De opties ontgrendelen veel gedrag in een enkele aanroep:125~126- **`idempotencyKey`** - als een run met dezelfde sleutel al bestaat, retourneert de SDK de bestaande handle in plaats van werk te dupliceren.127- **`concurrencyKey`** - serialiseert runs die de sleutel delen, zodat je een per-tenant rate limit niet overschrijdt.128- **`queue.concurrencyLimit`** - globale cap voor de queue over alle sleutels heen.129- **`delay`** - plant de run voor een toekomstige tijd.130- **`ttl`** - als de run tegen die tijd niet is gestart, automatisch verlopen.131~132### Batch trigger133~134Voor fan-out workloads accepteert `batchTrigger` tot 500 items per aanroep en creeert een run per 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## Geplande Tasks146~147Cron jobs worden eersterangs declaraties. De schedule zelf is een apart object dat je meerdere keren aan een task kunt koppelen.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~167Voor per-tenant schedules - zeg, een cron per klant - maak je ze dynamisch via de 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~181De `deduplicationKey` maakt de aanroep idempotent: dezelfde code opnieuw uitvoeren bij deploy-tijd stapelt geen dubbele schedules op.182~183## Queues, Concurrency en Idempotency184~185Drie primitieven dekken de meeste behoeften aan rate-limiting en ordering.186~187```mermaid188graph TB189 Trigger[trigger payload] --> IK{idempotencyKey<br/>gezien?}190 IK -->|ja| Reuse[Bestaande run retourneren]191 IK -->|nee| CK[concurrencyKey bucket]192 CK --> Q[Queue met<br/>concurrencyLimit]193 Q -->|slot beschikbaar| Run[Task uitvoeren]194 Q -->|slots vol| Wait[Wachten in queue]195```196~197Een 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.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## Waits en langlopend werk211~212Tasks 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.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` 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.230~231### Human-in-the-loop met `wait.forToken`232~233Voor approval-flows en AI-gates pauzeert `wait.forToken` tot je applicatie terugbelt met een resultaat.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~258De 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.259~260## Lifecycle Hooks261~262Je 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.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` draait een keer per worker container bij boot, niet per run, dus het is de juiste plek om clients en pools op te zetten.280~281## Realtime in de Frontend282~283Trigger.dev publiceert run-state-veranderingen - status, metadata, output - over een streaming API. De React-hooks abonneren zich op die stream en re-renderen automatisch.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~336Je 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.337~338Voor trigger-and-subscribe in een keer: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## AI-agents en Streaming354~355Trigger.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.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~384De 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.385~386## Deployen387~388Deploys compileren je tasks tot een versie-gemarkeerde bundle, bouwen een container en wisselen verkeer atomair. Oude in-flight runs blijven de vorige versie gebruiken.389~390```bash391npx trigger.dev@latest deploy --env prod392```393~394In CI verbind je dit doorgaans aan dezelfde workflow die je app verzendt: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~404Voor 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.405~406## Self-Hosting vs Cloud407~408Trigger.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.409~410| Aspect | Cloud | Self-hosted |411|--------|-------|-------------|412| **Setup** | Aanmelden, `init` uitvoeren | Docker-compose of Helm chart uitvoeren |413| **Scaling** | Automatisch | Jouw verantwoordelijkheid |414| **Pricing** | Per run + per compute | Alleen infra-kosten |415| **Compliance** | SOC 2 | Wat je omgeving biedt |416| **Beste voor** | De meeste teams | Strikte data residency, custom infra |417~418De SDK en CLI zijn identiek tussen modi - je verandert een profile flag en wijst naar je eigen instance.419~420## Best Practices421~422### 1. Houd payloads klein en serializeerbaar423~424Geef 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.425~426### 2. Idempotency keys op elke externe call427~428Combineer `idempotencyKey` op de task trigger met idempotency keys op je vendor-API's (Stripe, OpenAI, etc.). Retries zijn end-to-end veilig.429~430### 3. Gebruik `triggerAndWait` voor orchestratie, niet `Promise.all` van triggers431~432Een parent die `triggerAndWait` aanroept, componeert duurzaam child tasks. Een parent die triggert en onmiddellijk resolvet, verliest de observability van de keten.433~434### 4. Tag runs435~436Voeg `tags` toe aan triggers (`tags: ["user:123", "feature:onboarding"]`) zodat je het dashboard en de management API kunt filteren op zakelijke dimensies.437~438### 5. Houd `init` idempotent439~440Het draait bij elke cold start. Vermijd migraties of one-shot bijwerkingen daar.441~442## Conclusie443~444Trigger.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.445~446Hetzelfde 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.447~448> **Aan de slag Checklist:**449>450> - [x] Meld je aan op trigger.dev of draai de self-hosted Docker-stack451> - [x] `npx trigger.dev@latest init` in je project452> - [x] Definieer je eerste task met `task({ id, run })`453> - [x] Trigger het vanuit je API en bekijk de run in het dashboard454> - [x] Voeg `idempotencyKey` en `concurrencyKey` toe voor productieveiligheid455> - [x] Verbind `useRealtimeRun` met een statuscomponent456> - [x] Deploy met `trigger.dev deploy --env prod` vanuit CI457~
NORMAL · trigger-dev-background-jobs-guide.md [readonly]457 lines · :q to close