spinny:~/writing $ less trigger-dev-background-jobs-guide.md
12Die meisten Produktionsanwendungen brauchen Arbeit, die nicht in den Request/Response-Zyklus passt: E-Mails versenden, Uploads verarbeiten, KI-Pipelines ausfuhren, Drittanbieter-Daten synchronisieren, Berichte generieren. Die traditionelle Antwort ist eine Queue (Redis, SQS, RabbitMQ), eine Worker-Flotte, ein Scheduler und ein zerbrechlicher Stapel von Glue-Code, der bei jedem Deploy bricht.34[Trigger.dev](https://trigger.dev) kollabiert diesen Stack in ein einziges TypeScript-SDK. Du schreibst Funktionen, rufst sie von uberall auf und die Plattform verwaltet Queueing, Retries, Observability, Scheduling und durable Execution. Tasks laufen so lange wie notig - kein 10-Sekunden-Serverless-Timeout, kein verlorenes Werk bei Redeploys.56## Warum Trigger.dev78Die Verschiebung 2026 ist Durable Execution. Workflows mussen Restarts, Crashes, Deploys und Rate Limits uberleben. Sie mussen auch Fortschritt in Echtzeit zur UI streamen und fur menschliche Eingaben pausieren. Trigger.dev wurde mit Version 3 um diese Anforderungen herum neu gebaut und erweitert weiterhin seine KI-Infrastruktur-Oberflache.910```mermaid11graph LR12 App[Deine App] -->|trigger| API[Trigger.dev API]13 API --> Queue[Durable Queue]14 Queue --> Worker[Worker Container]15 Worker -->|run task| Task[Dein Task-Code]16 Task -->|metadata| Realtime[Realtime Stream]17 Realtime --> UI[React UI]18 Worker --> Storage[Run State Store]19```2021Das Modell ist einfach: du definierst Tasks als Exports, das SDK nimmt sie auf, die Plattform plant und fuhrt sie in isolierten Containern aus und der Run-Status wird persistiert, sodass du fortsetzen, wiederholen und beobachten kannst.2223## Loslegen2425### Projekt initialisieren2627```bash28npx trigger.dev@latest login29npx trigger.dev@latest init30```3132Das erstellt eine `trigger.config.ts`-Datei und ein `trigger/`-Verzeichnis mit Beispiel-Tasks. Die Config-Datei ist die Source of Truth fur dein Projekt: welche Verzeichnisse Tasks enthalten, Build-Einstellungen, Lifecycle-Hooks und Runtime-Optionen.3334```typescript35// trigger.config.ts36import { defineConfig } from "@trigger.dev/sdk";3738export 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```5556### Tasks lokal ausfuhren5758```bash59npx trigger.dev@latest dev60```6162Der Dev-Server verbindet sich mit der Cloud, registriert deine Tasks und streamt Runs durch deinen lokalen Code. Du setzt Breakpoints in deinem Editor und triffst sie bei echten Triggers - dieselbe Schleife, die du in jedem normalen Node.js-Projekt verwenden wurdest.6364## Einen Task definieren6566Ein Task ist ein Objekt, das mit einer eindeutigen `id` und einer `run`-Funktion exportiert wird. Das SDK inspiziert Exports uber `dirs` und registriert sie automatisch.6768```typescript69// trigger/send-welcome-email.ts70import { task } from "@trigger.dev/sdk";71import { Resend } from "resend";7273const resend = new Resend(process.env.RESEND_API_KEY);7475export 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 });9091 if (error) throw error;92 return { messageId: data?.id };93 },94});95```9697Drei Dinge zu beachten:98991. **Kein Timeout im Run-Body.** Die Plattform verwaltet die Ausfuhrungszeit uber `maxDuration` in der Config, nicht zur Laufzeit.1002. **Throws sind Retries.** Das SDK fangt Exceptions ab und fuhrt erneut mit exponentiellem Backoff gemass der `retry`-Policy aus.1013. **Der Ruckgabewert wird persistiert.** Andere Tasks und dein Frontend konnen `run.output` von uberall lesen.102103## Tasks triggern104105Du rufst einen Task von deinem Backend, deinen API-Routen oder einem anderen Task auf.106107```typescript108import { sendWelcomeEmail } from "@/trigger/send-welcome-email";109110const 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);120121console.log(handle.id); // run_xyz - verwende dies, um Fortschritt zu verfolgen oder anzuzeigen122```123124Die Optionen entsperren in einem Aufruf viel Verhalten:125126- **`idempotencyKey`** - existiert bereits ein Run mit demselben Key, gibt das SDK das vorhandene Handle zuruck, statt Arbeit zu duplizieren.127- **`concurrencyKey`** - serialisiert Runs, die den Key teilen, sodass du kein per-Tenant-Rate-Limit uberschreitest.128- **`queue.concurrencyLimit`** - globaler Cap fur die Queue uber alle Keys.129- **`delay`** - plant den Run fur eine zukunftige Zeit.130- **`ttl`** - wenn der Run bis dahin nicht gestartet ist, lass ihn automatisch ablaufen.131132### Batch-Trigger133134Fur Fan-Out-Workloads akzeptiert `batchTrigger` bis zu 500 Items pro Aufruf und erstellt einen Run pro Item.135136```typescript137await sendWelcomeEmail.batchTrigger(138 newUsers.map((u) => ({139 payload: { email: u.email, name: u.name },140 options: { idempotencyKey: `welcome-${u.id}` },141 }))142);143```144145## Geplante Tasks146147Cron-Jobs werden zu erstklassigen Deklarationen. Der Schedule selbst ist ein separates Objekt, das du mehrfach an einen Task anhangen kannst.148149```typescript150// trigger/daily-digest.ts151import { schedules } from "@trigger.dev/sdk";152153export 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);161162 await sendDigestForDate(payload.timestamp);163 },164});165```166167Fur per-Tenant-Schedules - sagen wir, ein Cron pro Kunde - erstellst du sie dynamisch uber die Management-API.168169```typescript170import { schedules } from "@trigger.dev/sdk";171172await 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```180181Der `deduplicationKey` macht den Aufruf idempotent: dasselben Code zur Deploy-Zeit erneut auszufuhren, stapelt keine doppelten Schedules.182183## Queues, Concurrency und Idempotenz184185Drei Primitive decken die meisten Rate-Limiting- und Ordering-Bedurfnisse ab.186187```mermaid188graph TB189 Trigger[trigger payload] --> IK{idempotencyKey<br/>gesehen?}190 IK -->|ja| Reuse[Existierenden Run zuruckgeben]191 IK -->|nein| CK[concurrencyKey Bucket]192 CK --> Q[Queue mit<br/>concurrencyLimit]193 Q -->|Slot frei| Run[Task ausfuhren]194 Q -->|Slots voll| Wait[In Queue warten]195```196197Ein gangiges Pattern: eine Queue pro Tenant mit einer kleinen per-Key-Concurrency, um das Rate-Limit eines Vendors zu respektieren, plus ein Idempotency-Key, um Retries sicher zu machen.198199```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```209210## Waits und langlaufende Arbeit211212Tasks konnen pausieren, ohne eine Verbindung zu halten oder Compute zu verbrennen. Die Plattform persistiert den State und setzt die Funktion fort, wenn der Wait abgeschlossen ist.213214```typescript215import { wait } from "@trigger.dev/sdk";216217export 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```228229`triggerAndWait` ist das Killer-Feature: es triggert einen Child-Task und suspendiert den Parent, bis der Child abgeschlossen ist. Du komponierst Tasks wie async-Funktionen, aber die Orchestrierung lauft durable uber Tage oder Wochen.230231### Human-in-the-Loop mit `wait.forToken`232233Fur Approval-Flows und KI-Gates pausiert `wait.forToken`, bis deine Anwendung mit einem Ergebnis zuruckruft.234235```typescript236import { task, wait } from "@trigger.dev/sdk";237238export const publishPost = task({239 id: "publish-post",240 run: async (payload: { draftId: string }) => {241 const draft = await generateAIContent(payload.draftId);242243 const token = await wait.createToken({ timeout: "7d" });244 await notifyEditor({ draftId: draft.id, token: token.id });245246 const decision = await wait.forToken<{ approved: boolean; notes?: string }>(247 token.id248 );249250 if (decision.approved) {251 return await publish(draft);252 }253 return await applyFeedback(draft, decision.notes);254 },255});256```257258Der Editor offnet eine UI, uberpruft den Entwurf, klickt auf Approve und dein Backend schliesst das Token ab. Der Task setzt dort fort, wo er aufgehort hat - selbst wenn Stunden oder Tage vergangen sind.259260## Lifecycle-Hooks261262Du kannst `init`, `onStart`, `onSuccess` und `onFailure` an einen Task oder global in `trigger.config.ts` anhangen. Verwende sie fur Tracing, Error-Reporting und gemeinsames Setup.263264```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```278279`init` lauft einmal pro Worker-Container beim Boot, nicht pro Run, also ist es der richtige Ort, um Clients und Pools einzurichten.280281## Realtime im Frontend282283Trigger.dev publiziert Run-State-Anderungen - Status, Metadaten, Output - uber eine Streaming-API. Die React-Hooks abonnieren diesen Stream und re-rendern automatisch.284285```typescript286// trigger/process-video.ts287import { task, metadata } from "@trigger.dev/sdk";288289export const processVideo = task({290 id: "process-video",291 run: async (payload: { videoId: string }) => {292 metadata.set("stage", "transcoding");293 await transcode(payload.videoId);294295 metadata.set("stage", "thumbnails");296 await generateThumbnails(payload.videoId);297298 metadata.set("stage", "uploading");299 const url = await uploadToCDN(payload.videoId);300301 return { url };302 },303});304```305306```tsx307// components/VideoStatus.tsx308"use client";309import { useRealtimeRun } from "@trigger.dev/react-hooks";310import type { processVideo } from "@/trigger/process-video";311312export 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 });322323 if (error) return <p>Error: {error.message}</p>;324 if (!run) return <p>Loading...</p>;325326 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```335336Du generierst das Public-Access-Token serverseitig, scoped auf einen bestimmten Run, und schickst es an den Client. Der Hook verwaltet Auth, Reconnection und inkrementelle Updates.337338Fur Trigger-and-Subscribe in einem Schritt:339340```tsx341import { useRealtimeTaskTrigger } from "@trigger.dev/react-hooks";342343const { submit, run, isLoading } = useRealtimeTaskTrigger<typeof processVideo>(344 "process-video",345 { accessToken: publicAccessToken }346);347348<button onClick={() => submit({ videoId })} disabled={isLoading}>349 Process video350</button>;351```352353## KI-Agenten und Streaming354355Trigger.dev ist eine beliebte Runtime fur KI-Agenten geworden, weil dieselben Primitive - durable Execution, Retries, Waits, Echtzeit-Metadaten, Human-in-the-Loop - genau das sind, was Agenten brauchen. Du streamst Tokens von einem Modell-Provider in `metadata`, wahrend der Run lauft, das Frontend rendert sie live und der Run uberlebt langlaufende Tool-Calls, ohne ein Serverless-Timeout zu verbrennen.356357```typescript358import { task, metadata } from "@trigger.dev/sdk";359import { streamText } from "ai";360import { anthropic } from "@ai-sdk/anthropic";361362export 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 });372373 let fullText = "";374 for await (const chunk of result.textStream) {375 fullText += chunk;376 metadata.set("partial", fullText);377 }378379 return { answer: fullText, usage: await result.usage };380 },381});382```383384Das Frontend verwendet `useRealtimeRun` und liest `run.metadata.partial`, um die Streaming-Antwort zu rendern, genauso wie du eine Chat-Completion rendern wurdest - ausser, dass diese einen vollstandigen Page-Reload uberlebt.385386## Deployen387388Deploys kompilieren deine Tasks zu einem versionierten Bundle, bauen einen Container und tauschen den Verkehr atomar. Alte In-Flight-Runs verwenden weiterhin die vorherige Version.389390```bash391npx trigger.dev@latest deploy --env prod392```393394In CI verbindest du dies typischerweise mit demselben Workflow, der deine App ausliefert:395396```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```403404Fur Preview-Umgebungen ubergib `--env preview --branch ${{ github.head_ref }}` und Trigger.dev erstellt eine isolierte Umgebung pro Branch, die widerspiegelt, wie Vercel Preview-Deployments handhabt.405406## Self-Hosting vs Cloud407408Trigger.dev ist Open Source unter der Apache-2.0-Lizenz. Du kannst auf jeder Container-Plattform self-hosten (Docker Compose, Kubernetes, Fly.io) oder die verwaltete Cloud auf trigger.dev verwenden.409410| Aspekt | Cloud | Self-hosted |411|--------|-------|-------------|412| **Setup** | Anmelden, `init` ausfuhren | Docker-Compose oder Helm-Chart ausfuhren |413| **Scaling** | Automatisch | Deine Verantwortung |414| **Pricing** | Pro Run + pro Compute | Nur Infra-Kosten |415| **Compliance** | SOC 2 | Was deine Umgebung bietet |416| **Ideal fur** | Die meisten Teams | Strenge Datenresidenz, Custom-Infra |417418Das SDK und die CLI sind zwischen den Modi identisch - du anderst ein Profil-Flag und zeigst auf deine eigene Instanz.419420## Best Practices421422### 1. Halte Payloads klein und serialisierbar423424Ubergib IDs und Referenzen, keine vollstandigen Objekte. Lade die Daten innerhalb des Tasks. Das halt die Queue klein, Payloads sind gunstig zu loggen und du kannst die Datenquelle andern, ohne erneut zu triggern.425426### 2. Idempotency-Keys bei jedem externen Call427428Kombiniere `idempotencyKey` beim Task-Trigger mit Idempotency-Keys bei deinen Vendor-APIs (Stripe, OpenAI, etc.). Retries sind end-to-end sicher.429430### 3. Verwende `triggerAndWait` fur Orchestrierung, nicht `Promise.all` von Triggers431432Ein Parent, der `triggerAndWait` aufruft, komponiert durable Child-Tasks. Ein Parent, der triggert und sofort auflost, verliert die Observability der Kette.433434### 4. Tagge Runs435436Fuge Triggers `tags` hinzu (`tags: ["user:123", "feature:onboarding"]`), damit du das Dashboard und die Management-API nach Geschaftsdimensionen filtern kannst.437438### 5. Halte `init` idempotent439440Es lauft bei jedem Cold Start. Vermeide Migrationen oder One-Shot-Seiteneffekte dort.441442## Fazit443444Trigger.dev entfernt die Kategorien von Arbeit, fur die fruher ein Job-System von Grund auf gebaut werden musste. Du schreibst async TypeScript, rufst es von uberall auf und die Plattform gibt dir durable Execution, Scheduling, Queues, Retries, Echtzeit-Updates und Human-in-the-Loop-Patterns out of the Box.445446Dieselbe Oberflache, die einen nachtlichen Cron antreibt, ist die Oberflache, die einen mehrstufigen KI-Agenten antreibt, der zum Frontend streamt und fur Review pausiert. Diese Konvergenz ist das, was das Framework 2026 einer ernsthaften Betrachtung wert macht, egal ob du ein SaaS betreibst, das zuverlassige Hintergrundarbeit braucht, oder KI-Features auslieferst, die ein Serverless-Timeout uberleben.447448> **Loslege-Checkliste:**449>450> - [x] Bei trigger.dev anmelden oder den self-hosted Docker-Stack ausfuhren451> - [x] `npx trigger.dev@latest init` in deinem Projekt452> - [x] Definiere deinen ersten Task mit `task({ id, run })`453> - [x] Triggere ihn aus deiner API und beobachte den Run im Dashboard454> - [x] Fuge `idempotencyKey` und `concurrencyKey` fur Produktionssicherheit hinzu455> - [x] Verbinde `useRealtimeRun` mit einer Status-Komponente456> - [x] Deploye mit `trigger.dev deploy --env prod` aus CI457
:Trigger.dev: Dauerhafte Hintergrundjobs und KI-Workflows in TypeScriptlines 1-457 (END) — press q to close