Wiekszosc aplikacji produkcyjnych potrzebuje pracy, ktora nie pasuje do cyklu zadanie/odpowiedz: wysylanie e-maili, przetwarzanie uploadow, uruchamianie pipeline'ow AI, synchronizacja danych zewnetrznych, generowanie raportow. Tradycyjna odpowiedz to kolejka (Redis, SQS, RabbitMQ), flota workerow, harmonogram i kruchy stos kodu spawajacego, ktory rozpada sie przy kazdym deployu.
Trigger.dev skraca ten stack do jednego SDK TypeScript. Piszesz funkcje, wywolujesz je z dowolnego miejsca i platforma zajmuje sie kolejkowaniem, ponowieniami, obserwowalnoscia, harmonogramowaniem i trwalym wykonywaniem. Tasks dzialaja tak dlugo, jak potrzeba - zaden 10-sekundowy timeout serverless, zaden zgubiony przy redeployu.
Dlaczego Trigger.dev
Zmiana w 2026 to trwale wykonywanie. Przeplywy musza przetrwac restarty, awarie, deploye i rate limity. Musza tez transmitowac postep do UI w czasie rzeczywistym i zatrzymywac sie na input czlowieka. Trigger.dev zostal przebudowany wokol tych wymagan w wersji 3 i nadal rozszerza swoja powierzchnie infrastruktury AI.
Model jest prosty: definiujesz tasks jako exports, SDK je odbiera, platforma planuje i uruchamia je w izolowanych kontenerach, a stan run jest przechowywany, abys mogl wznawiac, ponawiac i obserwowac.
Pierwsze kroki
Inicjalizuj projekt
npx trigger.dev@latest login npx trigger.dev@latest init
To tworzy plik trigger.config.ts i katalog trigger/ z przykladowymi tasks. Plik konfiguracyjny jest zrodlem prawdy dla twojego projektu: ktore katalogi zawieraja tasks, ustawienia builda, lifecycle hooks i opcje runtime.
// 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"], });
Uruchamiaj tasks lokalnie
npx trigger.dev@latest dev
Server dev laczy sie z chmura, rejestruje twoje tasks i transmituje runs przez twoj lokalny kod. Ustawiasz breakpointy w edytorze i trafiasz w nie na prawdziwych triggerach - ta sama petla, ktorej uzywalbys w jakimkolwiek normalnym projekcie Node.js.
Definiowanie Task
Task to obiekt eksportowany z unikatowym id i funkcja run. SDK inspekcjonuje exporty w dirs i automatycznie je rejestruje.
// 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 }; }, });
Trzy rzeczy do zauwazenia:
- Brak timeoutu w ciele run. Platforma zarzadza czasem wykonania przez
maxDurationw configu, nie w runtime. - Throws to retries. SDK lapie wyjatki i ponawia z exponential backoff zgodnie z polityka
retry. - Wartosc zwracana jest przechowywana. Inne tasks i twoj frontend moga czytac
run.outputskadkolwiek.
Triggerowanie Tasks
Wywolujesz task z backendu, route'ow API lub innego task'a.
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 - uzyj do sledzenia lub wyswietlania postepu
Opcje odblokowuja wiele zachowania w jednym wywolaniu:
idempotencyKey- jesli run z ta sama kluczem juz istnieje, SDK zwraca istniejacy uchwyt zamiast duplikowac prace.concurrencyKey- serializuje runs udostepniajace klucz, abys nie przekroczyl rate limitu na tenanta.queue.concurrencyLimit- globalna granica dla kolejki we wszystkich kluczach.delay- planuje run na przyszly czas.ttl- jesli run nie rozpoczal sie do tego czasu, automatycznie wygasa.
Batch trigger
Dla obciazen fan-out, batchTrigger przyjmuje do 500 elementow na wywolanie i tworzy jeden run na element.
await sendWelcomeEmail.batchTrigger( newUsers.map((u) => ({ payload: { email: u.email, name: u.name }, options: { idempotencyKey: `welcome-${u.id}` }, })) );
Tasks zaplanowane
Cron joby staja sie deklaracjami pierwszej klasy. Sam harmonogram to oddzielny obiekt, ktory mozesz przylaczyc do task'a wiele razy.
// 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); }, });
Dla harmonogramow na tenanta - powiedzmy, jeden cron na klienta - tworzysz je dynamicznie przez 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}`, });
deduplicationKey czyni wywolanie idempotentnym: ponowne uruchomienie tego samego kodu w czasie deployu nie nawarstwia zduplikowanych harmonogramow.
Kolejki, Wspolbieznosc i Idempotentnosc
Trzy primitywy pokrywaja wiekszosc potrzeb rate-limitingu i porzadkowania.
Powszechny wzorzec: jedna kolejka na tenanta z mala wspolbieznoscia na klucz, aby uszanowac rate limit dostawcy, plus klucz idempotentnosci, aby retries byly bezpieczne.
await syncShopifyOrders.trigger( { shopId }, { queue: { name: `shopify-${shopId}`, concurrencyLimit: 2 }, concurrencyKey: shopId, idempotencyKey: `sync-${shopId}-${Date.now() / 60_000 | 0}`, } );
Oczekiwania i praca dlugoterminowa
Tasks moga sie zatrzymac bez utrzymywania polaczenia czy spalania compute. Platforma utrzymuje stan i wznawia funkcje, gdy oczekiwanie sie konczy.
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 to zabojcza funkcja: triggeruje task dziecka i zawiesza rodzica az dziecko sie zakonczy. Komponujesz tasks jak funkcje async, ale orkiestracja dziala trwale przez dni lub tygodnie.
Human-in-the-loop z wait.forToken
Dla przeplywow zatwierdzania i bramek AI, wait.forToken zatrzymuje sie az twoja aplikacja oddzwoni z wynikiem.
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); }, });
Edytor otwiera UI, przeglada draft, klika Zatwierdz i twoj backend kompletuje token. Task wznawia od miejsca, w ktorym sie zatrzymal - nawet jesli minely godziny lub dni.
Lifecycle Hooks
Mozesz przylaczyc init, onStart, onSuccess i onFailure do task'a lub globalnie w trigger.config.ts. Uzywaj ich do tracingu, error reportingu i wspolnego setupu.
// 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 dziala raz na worker container przy bootcie, nie na run, wiec jest to dobre miejsce do skonfigurowania klientow i pul.
Realtime w Frontendzie
Trigger.dev publikuje zmiany stanu run - status, metadata, output - przez streaming API. Hooki React subskrybuja ten strumien i automatycznie re-renderuja.
// 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> ); }
Generujesz publiczny access token po stronie serwera, scoped do konkretnego run, i wysylasz do klienta. Hook obsluguje auth, ponowne polaczenie i przyrostowe aktualizacje.
Dla trigger-and-subscribe w jednym kroku:
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>;
Agenci AI i Streaming
Trigger.dev stal sie popularnym runtime dla agentow AI poniewaz te same primitywy - trwale wykonywanie, retries, oczekiwania, real-time metadata, human-in-the-loop - to dokladnie to, czego potrzebuja agenci. Strumieniujesz tokeny od dostawcy modelu do metadata podczas gdy run sie dzieje, frontend renderuje je na zywo, a run przezywa dlugotrwale tool calls bez spalania serverless timeoutu.
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 }; }, });
Frontend uzywa useRealtimeRun i czyta run.metadata.partial, aby renderowac odpowiedz strumieniowa, w ten sam sposob, w jaki renderowalbys chat completion - z wyjatkiem tego, ze ta przezywa pelny reload strony.
Deploying
Deploye kompiluja twoje tasks do bundla z wersja, buduja kontener i atomowo zamieniaja ruch. Stare runs w locie nadal uzywaja poprzedniej wersji.
npx trigger.dev@latest deploy --env prod
W CI zwykle podlaczasz to do tego samego workflow, ktory wysyla twoja aplikacje:
# .github/workflows/deploy.yml - name: Deploy Trigger.dev env: TRIGGER_ACCESS_TOKEN: ${{ secrets.TRIGGER_ACCESS_TOKEN }} run: npx trigger.dev@latest deploy --env prod
Dla srodowisk preview, przekaz --env preview --branch ${{ github.head_ref }} i Trigger.dev tworzy izolowane srodowisko na branch, odzwierciedlajac sposob, w jaki Vercel obsluguje preview deploymenty.
Self-Hosting vs Cloud
Trigger.dev jest open source na licencji Apache 2.0. Mozesz self-hostowac na dowolnej platformie kontenerowej (Docker Compose, Kubernetes, Fly.io) lub uzywac zarzadzanej chmury w trigger.dev.
| Aspekt | Cloud | Self-hosted |
|---|---|---|
| Setup | Rejestracja, uruchom init | Uruchom docker-compose lub Helm chart |
| Skalowanie | Automatyczne | Twoja odpowiedzialnosc |
| Pricing | Za run + za compute | Tylko koszt infrastruktury |
| Compliance | SOC 2 | Co dostarcza twoje srodowisko |
| Najlepsze dla | Wiekszosc zespolow | Surowa rezydencja danych, infra niestandardowa |
SDK i CLI sa identyczne miedzy trybami - zmieniasz flage profilu i wskazujesz na wlasna instancje.
Best Practices
1. Trzymaj payloady male i serializowalne
Przekazuj IDki i referencje, nie pelne obiekty. Pobieraj dane wewnatrz task'a. Utrzymuje to kolejke mala, payloady tanio loggable i pozwala zmieniac zrodlo danych bez ponownego triggerowania.
2. Klucze idempotentnosci na kazdym wywolaniu zewnetrznym
Polacz idempotencyKey na triggerze task'a z kluczami idempotentnosci na API dostawcow (Stripe, OpenAI, itp.). Retries beda bezpieczne end-to-end.
3. Uzywaj triggerAndWait do orkiestracji, a nie Promise.all triggerow
Rodzic, ktory wywoluje triggerAndWait, trwale komponuje task dzieci. Rodzic, ktory triggeruje i resolvuje natychmiast, traci obserwowalnosc lancucha.
4. Taguj runs
Dodaj tags do triggerow (tags: ["user:123", "feature:onboarding"]), abys mogl filtrowac dashboard i management API wedlug wymiarow biznesowych.
5. Trzymaj init idempotentnym
Dziala przy kazdym cold starcie. Unikaj migracji lub jednorazowych skutkow ubocznych tam.
Wniosek
Trigger.dev usuwa kategorie pracy, ktore wczesniej wymagaly zbudowania systemu jobow od zera. Piszesz async TypeScript, wywolujesz go skadkolwiek i platforma daje ci trwale wykonywanie, harmonogramowanie, kolejki, retries, aktualizacje real-time i wzorce human-in-the-loop out of the box.
Ta sama powierzchnia, ktora napedza nocnego crona, jest powierzchnia, ktora napedza wieloetapowego agenta AI, ktory strumieniuje do frontendu i zatrzymuje sie na review. Ta konwergencja jest tym, co czyni framework wartym powaznego spojrzenia w 2026, niezaleznie od tego, czy zarzadzasz SaaS, ktory potrzebuje niezawodnej pracy w tle, czy wysylasz funkcje AI, ktore przezywaja serverless timeout.
Checklist Pierwszych Krokow:
- Zarejestruj sie na trigger.dev lub uruchom self-hosted Docker stack
npx trigger.dev@latest initw twoim projekcie- Zdefiniuj swoj pierwszy task z
task({ id, run })- Triggeruj go z twojego API i ogladaj run w dashboardzie
- Dodaj
idempotencyKeyiconcurrencyKeydla bezpieczenstwa produkcyjnego- Polacz
useRealtimeRunz komponentem statusu- Deployuj z
trigger.dev deploy --env prodz CI