Die 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.
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.
Warum Trigger.dev
Die 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.
Das 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.
Loslegen
Projekt initialisieren
npx trigger.dev@latest login npx trigger.dev@latest init
Das 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.
// 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"], });
Tasks lokal ausfuhren
npx trigger.dev@latest dev
Der 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.
Einen Task definieren
Ein 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.
// 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 }; }, });
Drei Dinge zu beachten:
- Kein Timeout im Run-Body. Die Plattform verwaltet die Ausfuhrungszeit uber
maxDurationin der Config, nicht zur Laufzeit. - Throws sind Retries. Das SDK fangt Exceptions ab und fuhrt erneut mit exponentiellem Backoff gemass der
retry-Policy aus. - Der Ruckgabewert wird persistiert. Andere Tasks und dein Frontend konnen
run.outputvon uberall lesen.
Tasks triggern
Du rufst einen Task von deinem Backend, deinen API-Routen oder einem anderen Task auf.
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 - verwende dies, um Fortschritt zu verfolgen oder anzuzeigen
Die Optionen entsperren in einem Aufruf viel Verhalten:
idempotencyKey- existiert bereits ein Run mit demselben Key, gibt das SDK das vorhandene Handle zuruck, statt Arbeit zu duplizieren.concurrencyKey- serialisiert Runs, die den Key teilen, sodass du kein per-Tenant-Rate-Limit uberschreitest.queue.concurrencyLimit- globaler Cap fur die Queue uber alle Keys.delay- plant den Run fur eine zukunftige Zeit.ttl- wenn der Run bis dahin nicht gestartet ist, lass ihn automatisch ablaufen.
Batch-Trigger
Fur Fan-Out-Workloads akzeptiert batchTrigger bis zu 500 Items pro Aufruf und erstellt einen Run pro Item.
await sendWelcomeEmail.batchTrigger( newUsers.map((u) => ({ payload: { email: u.email, name: u.name }, options: { idempotencyKey: `welcome-${u.id}` }, })) );
Geplante Tasks
Cron-Jobs werden zu erstklassigen Deklarationen. Der Schedule selbst ist ein separates Objekt, das du mehrfach an einen Task anhangen kannst.
// 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); }, });
Fur per-Tenant-Schedules - sagen wir, ein Cron pro Kunde - erstellst du sie dynamisch uber die 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}`, });
Der deduplicationKey macht den Aufruf idempotent: dasselben Code zur Deploy-Zeit erneut auszufuhren, stapelt keine doppelten Schedules.
Queues, Concurrency und Idempotenz
Drei Primitive decken die meisten Rate-Limiting- und Ordering-Bedurfnisse ab.
Ein 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.
await syncShopifyOrders.trigger( { shopId }, { queue: { name: `shopify-${shopId}`, concurrencyLimit: 2 }, concurrencyKey: shopId, idempotencyKey: `sync-${shopId}-${Date.now() / 60_000 | 0}`, } );
Waits und langlaufende Arbeit
Tasks 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.
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 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.
Human-in-the-Loop mit wait.forToken
Fur Approval-Flows und KI-Gates pausiert wait.forToken, bis deine Anwendung mit einem Ergebnis zuruckruft.
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); }, });
Der 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.
Lifecycle-Hooks
Du 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.
// 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 lauft einmal pro Worker-Container beim Boot, nicht pro Run, also ist es der richtige Ort, um Clients und Pools einzurichten.
Realtime im Frontend
Trigger.dev publiziert Run-State-Anderungen - Status, Metadaten, Output - uber eine Streaming-API. Die React-Hooks abonnieren diesen Stream und re-rendern 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> ); }
Du 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.
Fur Trigger-and-Subscribe in einem Schritt:
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>;
KI-Agenten und Streaming
Trigger.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.
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 }; }, });
Das 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.
Deployen
Deploys kompilieren deine Tasks zu einem versionierten Bundle, bauen einen Container und tauschen den Verkehr atomar. Alte In-Flight-Runs verwenden weiterhin die vorherige Version.
npx trigger.dev@latest deploy --env prod
In CI verbindest du dies typischerweise mit demselben Workflow, der deine App ausliefert:
# .github/workflows/deploy.yml - name: Deploy Trigger.dev env: TRIGGER_ACCESS_TOKEN: ${{ secrets.TRIGGER_ACCESS_TOKEN }} run: npx trigger.dev@latest deploy --env prod
Fur 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.
Self-Hosting vs Cloud
Trigger.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.
| Aspekt | Cloud | Self-hosted |
|---|---|---|
| Setup | Anmelden, init ausfuhren | Docker-Compose oder Helm-Chart ausfuhren |
| Scaling | Automatisch | Deine Verantwortung |
| Pricing | Pro Run + pro Compute | Nur Infra-Kosten |
| Compliance | SOC 2 | Was deine Umgebung bietet |
| Ideal fur | Die meisten Teams | Strenge Datenresidenz, Custom-Infra |
Das SDK und die CLI sind zwischen den Modi identisch - du anderst ein Profil-Flag und zeigst auf deine eigene Instanz.
Best Practices
1. Halte Payloads klein und serialisierbar
Ubergib 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.
2. Idempotency-Keys bei jedem externen Call
Kombiniere idempotencyKey beim Task-Trigger mit Idempotency-Keys bei deinen Vendor-APIs (Stripe, OpenAI, etc.). Retries sind end-to-end sicher.
3. Verwende triggerAndWait fur Orchestrierung, nicht Promise.all von Triggers
Ein Parent, der triggerAndWait aufruft, komponiert durable Child-Tasks. Ein Parent, der triggert und sofort auflost, verliert die Observability der Kette.
4. Tagge Runs
Fuge Triggers tags hinzu (tags: ["user:123", "feature:onboarding"]), damit du das Dashboard und die Management-API nach Geschaftsdimensionen filtern kannst.
5. Halte init idempotent
Es lauft bei jedem Cold Start. Vermeide Migrationen oder One-Shot-Seiteneffekte dort.
Fazit
Trigger.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.
Dieselbe 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.
Loslege-Checkliste:
- Bei trigger.dev anmelden oder den self-hosted Docker-Stack ausfuhren
npx trigger.dev@latest initin deinem Projekt- Definiere deinen ersten Task mit
task({ id, run })- Triggere ihn aus deiner API und beobachte den Run im Dashboard
- Fuge
idempotencyKeyundconcurrencyKeyfur Produktionssicherheit hinzu- Verbinde
useRealtimeRunmit einer Status-Komponente- Deploye mit
trigger.dev deploy --env prodaus CI