spinny:~/writing $ vim trigger-dev-background-jobs-guide.md
1~2Die 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.3~4[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.5~6## Warum Trigger.dev7~8Die 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.9~10```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```20~21Das 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.22~23## Loslegen24~25### Projekt initialisieren26~27```bash28npx trigger.dev@latest login29npx trigger.dev@latest init30```31~32Das 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.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### Tasks lokal ausfuhren57~58```bash59npx trigger.dev@latest dev60```61~62Der 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.63~64## Einen Task definieren65~66Ein 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.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~97Drei Dinge zu beachten:98~991. **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.102~103## Tasks triggern104~105Du rufst einen Task von deinem Backend, deinen API-Routen oder einem anderen Task auf.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 - verwende dies, um Fortschritt zu verfolgen oder anzuzeigen122```123~124Die Optionen entsperren in einem Aufruf viel Verhalten:125~126- **`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.131~132### Batch-Trigger133~134Fur Fan-Out-Workloads akzeptiert `batchTrigger` bis zu 500 Items pro Aufruf und erstellt einen Run pro 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## Geplante Tasks146~147Cron-Jobs werden zu erstklassigen Deklarationen. Der Schedule selbst ist ein separates Objekt, das du mehrfach an einen Task anhangen kannst.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~167Fur per-Tenant-Schedules - sagen wir, ein Cron pro Kunde - erstellst du sie dynamisch uber die 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~181Der `deduplicationKey` macht den Aufruf idempotent: dasselben Code zur Deploy-Zeit erneut auszufuhren, stapelt keine doppelten Schedules.182~183## Queues, Concurrency und Idempotenz184~185Drei Primitive decken die meisten Rate-Limiting- und Ordering-Bedurfnisse ab.186~187```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```196~197Ein 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.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 und langlaufende Arbeit211~212Tasks 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.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` 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.230~231### Human-in-the-Loop mit `wait.forToken`232~233Fur Approval-Flows und KI-Gates pausiert `wait.forToken`, bis deine Anwendung mit einem Ergebnis zuruckruft.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~258Der 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.259~260## Lifecycle-Hooks261~262Du 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.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` lauft einmal pro Worker-Container beim Boot, nicht pro Run, also ist es der richtige Ort, um Clients und Pools einzurichten.280~281## Realtime im Frontend282~283Trigger.dev publiziert Run-State-Anderungen - Status, Metadaten, Output - uber eine Streaming-API. Die React-Hooks abonnieren diesen Stream und re-rendern 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~336Du 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.337~338Fur Trigger-and-Subscribe in einem Schritt: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## KI-Agenten und Streaming354~355Trigger.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.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~384Das 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.385~386## Deployen387~388Deploys kompilieren deine Tasks zu einem versionierten Bundle, bauen einen Container und tauschen den Verkehr atomar. Alte In-Flight-Runs verwenden weiterhin die vorherige Version.389~390```bash391npx trigger.dev@latest deploy --env prod392```393~394In CI verbindest du dies typischerweise mit demselben Workflow, der deine App ausliefert: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~404Fur 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.405~406## Self-Hosting vs Cloud407~408Trigger.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.409~410| 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 |417~418Das SDK und die CLI sind zwischen den Modi identisch - du anderst ein Profil-Flag und zeigst auf deine eigene Instanz.419~420## Best Practices421~422### 1. Halte Payloads klein und serialisierbar423~424Ubergib 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.425~426### 2. Idempotency-Keys bei jedem externen Call427~428Kombiniere `idempotencyKey` beim Task-Trigger mit Idempotency-Keys bei deinen Vendor-APIs (Stripe, OpenAI, etc.). Retries sind end-to-end sicher.429~430### 3. Verwende `triggerAndWait` fur Orchestrierung, nicht `Promise.all` von Triggers431~432Ein Parent, der `triggerAndWait` aufruft, komponiert durable Child-Tasks. Ein Parent, der triggert und sofort auflost, verliert die Observability der Kette.433~434### 4. Tagge Runs435~436Fuge Triggers `tags` hinzu (`tags: ["user:123", "feature:onboarding"]`), damit du das Dashboard und die Management-API nach Geschaftsdimensionen filtern kannst.437~438### 5. Halte `init` idempotent439~440Es lauft bei jedem Cold Start. Vermeide Migrationen oder One-Shot-Seiteneffekte dort.441~442## Fazit443~444Trigger.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.445~446Dieselbe 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.447~448> **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~
NORMAL · trigger-dev-background-jobs-guide.md [readonly]457 lines · :q to close