spinny:~/writing $ vim trigger-dev-background-jobs-guide.md
1~2De fleste produktionsapplikationer har brug for arbejde, der ikke passer ind i request/response-cyklussen: afsendelse af e-mails, behandling af uploads, kørsel af AI-pipelines, synkronisering af tredjepartsdata, generering af rapporter. Det traditionelle svar er en koe (Redis, SQS, RabbitMQ), en flade af workers, en scheduler og en skroebelig bunke limkode, der gar i stykker ved hver deployment.3~4[Trigger.dev](https://trigger.dev) komprimerer den stack til et enkelt TypeScript SDK. Du skriver funktioner, kalder dem fra hvor som helst, og platformen handterer queueing, retries, observability, scheduling og holdbar udforsel. Tasks korer sa lange, der er brug for - ingen 10-sekunders serverless timeout, intet tabt arbejde ved redeploys.5~6## Hvorfor Trigger.dev7~8Skiftet i 2026 er holdbar udforsel. Workflows skal overleve genstarter, nedbrud, deploys og rate limits. De skal ogsa streame fremgang til UI'en i realtid og pause for menneskelig input. Trigger.dev blev genopbygget omkring disse krav med version 3 og fortsætter med at udvide sin AI-infrastrukturoverflade.9~10```mermaid11graph LR12 App[Din App] -->|trigger| API[Trigger.dev API]13 API --> Queue[Holdbar Koe]14 Queue --> Worker[Worker Container]15 Worker -->|run task| Task[Din Task-kode]16 Task -->|metadata| Realtime[Realtid Stream]17 Realtime --> UI[React UI]18 Worker --> Storage[Run State Store]19```20~21Modellen er enkel: du definerer tasks som exports, SDK'et henter dem, platformen planlaegger og kører dem i isolerede containere, og run-tilstanden bevares, sa du kan genoptage, prove igen og observere.22~23## Kom i gang24~25### Initialiser et projekt26~27```bash28npx trigger.dev@latest login29npx trigger.dev@latest init30```31~32Dette opretter en `trigger.config.ts` fil og en `trigger/` mappe med eksempel-tasks. Konfigurationsfilen er sandhedskilden for dit projekt: hvilke mapper indeholder tasks, byggeindstillinger, lifecycle hooks og runtime-muligheder.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### Kor tasks lokalt57~58```bash59npx trigger.dev@latest dev60```61~62Dev-serveren forbinder til skyen, registrerer dine tasks og streamer runs gennem din lokale kode. Du saetter breakpoints i din editor og rammer dem pa rigtige triggers - samme loop, du ville bruge i ethvert almindeligt Node.js-projekt.63~64## Definere en Task65~66En task er et objekt, der eksporteres med et unikt `id` og en `run` funktion. SDK'et inspicerer eksporterne paa tvers af `dirs` og registrerer dem automatisk.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~97Tre ting at bemaerke:98~991. **Ingen timeout i run-kroppen.** Platformen handterer udforselstiden via `maxDuration` i konfigurationen, ikke i runtime.1002. **Throws er retries.** SDK'et fanger undtagelser og korer igen med eksponentiel backoff i henhold til `retry`-politikken.1013. **Returvardien bevares.** Andre tasks og din frontend kan laese `run.output` hvor som helst.102~103## Trigge Tasks104~105Du kalder en task fra din backend, dine API-ruter eller en anden 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 - brug til at spore eller vise fremskridt122```123~124Mulighederne lasker meget adfaerd op i et enkelt opkald:125~126- **`idempotencyKey`** - hvis et run med samme noegle allerede eksisterer, returnerer SDK'et det eksisterende handle i stedet for at duplikere arbejde.127- **`concurrencyKey`** - serialiserer runs, der deler noeglen, sa du ikke overskrider en per-tenant rate limit.128- **`queue.concurrencyLimit`** - global cap for koen pa tvers af alle noegler.129- **`delay`** - planlaegger run for en fremtidig tid.130- **`ttl`** - hvis run ikke er startet inden da, udlober automatisk.131~132### Batch trigger133~134For fan-out-arbejdsbelastninger accepterer `batchTrigger` op til 500 elementer per opkald og opretter en run per element.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## Planlagte Tasks146~147Cron-jobs bliver foersteklasses erklaeringer. Selve schedule er et separat objekt, du kan vedhaefte til en task flere gange.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~167For per-tenant schedules - sig en cron per kunde - opretter du dem dynamisk via 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~181`deduplicationKey` gor opkaldet idempotent: at kore samme kode igen ved deploy-tid stabler ikke duplikerede schedules.182~183## Koer, Samtidighed og Idempotens184~185Tre primitiver daekker de fleste behov for rate-limiting og rakkefølge.186~187```mermaid188graph TB189 Trigger[trigger payload] --> IK{idempotencyKey<br/>set?}190 IK -->|ja| Reuse[Returner eksisterende run]191 IK -->|nej| CK[concurrencyKey bucket]192 CK --> Q[Koe med<br/>concurrencyLimit]193 Q -->|slot tilgaengelig| Run[Kor task]194 Q -->|slots fulde| Wait[Vent i koen]195```196~197Et almindeligt monster: en koe per tenant med en lille per-key samtidighed for at respektere en vendors rate limit, plus en idempotens noegle for at gore retries sikre.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## Ventetid og langvarigt arbejde211~212Tasks kan pause uden at holde en forbindelse eller braende compute. Platformen bevarer tilstanden og genoptager funktionen, naar ventetiden er afsluttet.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` er killer-funktionen: den triggrer en barntask og suspenderer foralderen, indtil barnet er faerdigt. Du komponerer tasks som async-funktioner, men orkestrationen korer holdbart over dage eller uger.230~231### Human-in-the-loop med `wait.forToken`232~233For godkendelsesstromninger og AI-gates pauser `wait.forToken`, indtil din applikation kalder tilbage med et resultat.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~258Redaktoren abner et UI, gennemgar udkastet, klikker pa Godkend, og din backend afslutter token. Task fortsaetter, hvor den slap - selv hvis timer eller dage er gaet.259~260## Lifecycle Hooks261~262Du kan vedhaefte `init`, `onStart`, `onSuccess` og `onFailure` til en task eller globalt i `trigger.config.ts`. Brug dem til tracing, error reporting og delt 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` kører en gang per worker container ved boot, ikke per run, sa det er det rigtige sted at opsaette klienter og pools.280~281## Realtid i Frontenden282~283Trigger.dev publicerer run state-aendringer - status, metadata, output - over et streaming API. React-hooks abonnerer pa den stream og re-renderer automatisk.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 genererer det offentlige access token pa serversiden, scoped til en specifik run, og sender det til klienten. Hooket handterer auth, gentilslutning og inkrementelle opdateringer.337~338For trigger-and-subscribe i et trin: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-agenter og Streaming354~355Trigger.dev er blevet en populaer runtime for AI-agenter, fordi de samme primitiver - holdbar udforsel, retries, ventetid, realtidsmetadata, human-in-the-loop - er praecis det, agenter har brug for. Du streamer tokens fra en model-udbyder til `metadata`, mens run finder sted, frontenden renderer dem live, og run overlever langvarige tool calls uden at braende et serverless timeout.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~384Frontenden bruger `useRealtimeRun` og laeser `run.metadata.partial` for at rendere streaming-svaret, pa samme made som du ville rendere en chat completion - undtagen at denne overlever en fuld side-genindlaesning.385~386## Deploye387~388Deployments kompilerer dine tasks til en versioneret bundle, bygger en container og bytter trafik atomart. Gamle in-flight runs fortsætter med at bruge den tidligere version.389~390```bash391npx trigger.dev@latest deploy --env prod392```393~394I CI forbinder du typisk dette til samme workflow, der sender din app: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~404For preview-miljoer, send `--env preview --branch ${{ github.head_ref }}` og Trigger.dev opretter et isoleret miljo per branch, hvilket afspejler den made, hvorpa Vercel handterer preview-deployments.405~406## Self-Hosting vs Cloud407~408Trigger.dev er open source under Apache 2.0-licensen. Du kan self-hoste pa enhver containerplatform (Docker Compose, Kubernetes, Fly.io) eller bruge den administrerede sky pa trigger.dev.409~410| Aspekt | Cloud | Self-hosted |411|--------|-------|-------------|412| **Setup** | Tilmeld, kor `init` | Kor docker-compose eller Helm chart |413| **Skalering** | Automatisk | Dit ansvar |414| **Pricing** | Per run + per compute | Kun infra-omkostning |415| **Compliance** | SOC 2 | Hvad dit miljo leverer |416| **Bedst til** | De fleste teams | Streng dataresidens, custom infra |417~418SDK'et og CLI'en er identiske mellem tilstande - du aendrer et profile flag og peger pa din egen instans.419~420## Best Practices421~422### 1. Hold payloads sma og serialiserbare423~424Send IDer og referencer, ikke fulde objekter. Hent data inde i task. Dette holder koen lille, payloads billige at logge og lader dig aendre datakilden uden at trigge igen.425~426### 2. Idempotens-noegler pa hvert eksternt opkald427~428Kombiner `idempotencyKey` pa task-trigger med idempotens-noegler pa dine vendor-API'er (Stripe, OpenAI, etc.). Retries bliver end-to-end sikre.429~430### 3. Brug `triggerAndWait` til orkestrering, ikke `Promise.all` af triggers431~432En forelder, der kalder `triggerAndWait`, komponerer holdbart child tasks. En forelder, der trigger og loeser med det samme, mister observerbarhed af kaeden.433~434### 4. Tag runs435~436Tilfojer `tags` til triggers (`tags: ["user:123", "feature:onboarding"]`), sa du kan filtrere dashboard og management API efter forretningsdimensioner.437~438### 5. Hold `init` idempotent439~440Den korer ved hver cold start. Undga migreringer eller engangs-bivirkninger der.441~442## Konklusion443~444Trigger.dev fjerner de kategorier af arbejde, der tidligere kraevede at bygge et job-system fra bunden. Du skriver async TypeScript, du kalder det fra hvor som helst, og platformen giver dig holdbar udforsel, scheduling, koer, retries, realtidsopdateringer og human-in-the-loop monstre ud af kassen.445~446Den samme overflade, der driver en natlig cron, er den overflade, der driver en multi-step AI-agent, der streamer til frontenden og pauser for review. Den konvergens er det, der gor framework vaerd at se serioest pa i 2026, uanset om du driver en SaaS, der har brug for paalideligt baggrundsarbejde, eller sender AI-funktioner, der overlever en serverless timeout.447~448> **Komme i gang Checklist:**449>450> - [x] Tilmeld dig pa trigger.dev eller kor self-hosted Docker stack451> - [x] `npx trigger.dev@latest init` i dit projekt452> - [x] Definer din forste task med `task({ id, run })`453> - [x] Trigge den fra din API og se run i dashboardet454> - [x] Tilfojer `idempotencyKey` og `concurrencyKey` for produktionssikkerhed455> - [x] Forbind `useRealtimeRun` til en statuskomponent456> - [x] Deploy med `trigger.dev deploy --env prod` fra CI457~
NORMAL · trigger-dev-background-jobs-guide.md [readonly]457 lines · :q to close