spinny:~/writing $ less trigger-dev-background-jobs-guide.md
12La mayoria de las aplicaciones en produccion necesitan trabajo que no encaja en el ciclo peticion/respuesta: enviar emails, procesar uploads, ejecutar pipelines de IA, sincronizar datos de terceros, generar reportes. La respuesta tradicional es una cola (Redis, SQS, RabbitMQ), una flota de workers, un scheduler y una pila fragil de codigo glue que se rompe en cada deploy.34[Trigger.dev](https://trigger.dev) colapsa esa stack en un unico SDK de TypeScript. Escribes funciones, las llamas desde cualquier lugar y la plataforma maneja el queueing, los retries, la observabilidad, el scheduling y la ejecucion duradera. Las tasks corren todo el tiempo necesario - sin timeout serverless de 10 segundos, sin trabajo perdido en redeploys.56## Por que Trigger.dev78El cambio en 2026 es la ejecucion duradera. Los workflows deben sobrevivir a reinicios, crashes, deploys y rate limits. Tambien deben streamear el progreso a la UI en tiempo real y pausarse para input humano. Trigger.dev fue reconstruido alrededor de estos requisitos con la version 3 y continua expandiendo su superficie de infraestructura para IA.910```mermaid11graph LR12 App[Tu App] -->|trigger| API[API Trigger.dev]13 API --> Queue[Cola Duradera]14 Queue --> Worker[Container Worker]15 Worker -->|run task| Task[Codigo de tu Task]16 Task -->|metadata| Realtime[Stream Realtime]17 Realtime --> UI[UI React]18 Worker --> Storage[Store Estado Run]19```2021El modelo es simple: defines tasks como exports, el SDK las recoge, la plataforma las planifica y ejecuta en containers aislados y el estado del run se persiste para que puedas reanudar, reintentar y observar.2223## Para empezar2425### Inicializar un proyecto2627```bash28npx trigger.dev@latest login29npx trigger.dev@latest init30```3132Esto crea un archivo `trigger.config.ts` y un directorio `trigger/` con tasks de ejemplo. El archivo de config es la fuente de verdad para tu proyecto: que directorios contienen tasks, ajustes de build, lifecycle hooks y opciones de runtime.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### Ejecutar tasks localmente5758```bash59npx trigger.dev@latest dev60```6162El servidor de dev se conecta a la cloud, registra tus tasks y streamea los runs a traves de tu codigo local. Pones breakpoints en tu editor y los alcanzas con triggers reales - el mismo loop que usarias en cualquier proyecto Node.js normal.6364## Definir una Task6566Una task es un objeto exportado con un `id` unico y una funcion `run`. El SDK inspecciona los exports a traves de `dirs` y los registra automaticamente.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```9697Tres cosas a notar:98991. **Sin timeout en el cuerpo del run.** La plataforma gestiona el tiempo de ejecucion via `maxDuration` en config, no en el runtime.1002. **Los throws son retries.** El SDK captura excepciones y reejecuta con backoff exponencial segun la policy `retry`.1013. **El valor de retorno se persiste.** Otras tasks y tu frontend pueden leer `run.output` desde cualquier lugar.102103## Triggerear Tasks104105Llamas a una task desde tu backend, tus rutas API u otra task.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 - usalo para rastrear o mostrar el progreso122```123124Las opciones desbloquean mucho comportamiento en una sola llamada:125126- **`idempotencyKey`** - si un run con la misma key ya existe, el SDK retorna el handle existente en vez de duplicar trabajo.127- **`concurrencyKey`** - serializa runs que comparten la key para que no excedas un rate limit per-tenant.128- **`queue.concurrencyLimit`** - cap global para la cola a traves de todas las keys.129- **`delay`** - planifica el run para un tiempo futuro.130- **`ttl`** - si el run no ha empezado para entonces, expiralo automaticamente.131132### Batch trigger133134Para workloads de fan-out, `batchTrigger` acepta hasta 500 items por llamada y crea un run por 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## Tasks Programadas146147Los cron jobs se vuelven declaraciones de primera clase. El schedule en si es un objeto separado que puedes adjuntar a una task multiples veces.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```166167Para schedules per-tenant - digamos, un cron por cliente - los creas dinamicamente a traves de la 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```180181La `deduplicationKey` hace la llamada idempotente: reejecutar el mismo codigo en deploy time no apila schedules duplicados.182183## Colas, Concurrencia e Idempotencia184185Tres primitivas cubren la mayoria de las necesidades de rate-limiting y ordering.186187```mermaid188graph TB189 Trigger[trigger payload] --> IK{idempotencyKey<br/>vista?}190 IK -->|si| Reuse[Retornar run existente]191 IK -->|no| CK[bucket concurrencyKey]192 CK --> Q[Cola con<br/>concurrencyLimit]193 Q -->|slot disponible| Run[Ejecutar task]194 Q -->|slots llenos| Wait[Esperar en cola]195```196197Un patron comun: una cola por tenant con una pequena concurrencia per-key para respetar el rate limit de un vendor, mas una idempotency key para hacer los retries seguros.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## Esperas y Trabajo de Larga Duracion211212Las tasks pueden pausarse sin mantener una conexion o quemar compute. La plataforma persiste el estado y reanuda la funcion cuando la espera se completa.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` es la feature decisiva: triggea una task hijo y suspende al padre hasta que el hijo se completa. Compones tasks como funciones async, pero la orquestacion corre duraderamente a traves de dias o semanas.230231### Human-in-the-loop con `wait.forToken`232233Para flujos de aprobacion y gates de IA, `wait.forToken` pausa hasta que tu aplicacion responda con un resultado.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```257258El editor abre una UI, revisa el borrador, hace clic en Aprobar y tu backend completa el token. La task continua donde se quedo - incluso si han pasado horas o dias.259260## Lifecycle Hooks261262Puedes adjuntar `init`, `onStart`, `onSuccess` y `onFailure` a una task o globalmente en `trigger.config.ts`. Usalos para tracing, error reporting y setup compartido.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` corre una vez por container worker en el boot, no por run, asi que es el lugar correcto para configurar clientes y pools.280281## Realtime en el Frontend282283Trigger.dev publica los cambios de estado del run - status, metadata, output - sobre una API en streaming. Los hooks React se suscriben a ese stream y rerenderizan automaticamente.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```335336Generas el public access token del lado del servidor, scoped a un run especifico, y lo envias al cliente. El hook maneja auth, reconexion y actualizaciones incrementales.337338Para trigger-and-subscribe en un solo paso: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## Agentes IA y Streaming354355Trigger.dev se ha convertido en un runtime popular para agentes IA porque las mismas primitivas - ejecucion duradera, retries, esperas, metadata en tiempo real, human-in-the-loop - son exactamente lo que los agentes necesitan. Streameas tokens de un proveedor de modelos a `metadata` mientras el run esta sucediendo, el frontend los renderiza en vivo y el run sobrevive a tool calls de larga duracion sin quemar un timeout serverless.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```383384El frontend usa `useRealtimeRun` y lee `run.metadata.partial` para renderizar la respuesta en streaming, de la misma forma en que renderizarias una chat completion - excepto que esta sobrevive a una recarga completa de la pagina.385386## Deployando387388Los deploys compilan tus tasks a un bundle versionado, construyen un container y hacen swap atomico del trafico. Los runs viejos en vuelo siguen usando la version anterior.389390```bash391npx trigger.dev@latest deploy --env prod392```393394En CI tipicamente conectas esto al mismo workflow que envia tu app: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```403404Para entornos de preview, pasa `--env preview --branch ${{ github.head_ref }}` y Trigger.dev crea un entorno aislado por branch, reflejando como Vercel maneja los preview deployments.405406## Self-Hosting vs Cloud407408Trigger.dev es open source bajo la licencia Apache 2.0. Puedes self-hostear en cualquier plataforma de containers (Docker Compose, Kubernetes, Fly.io) o usar la cloud gestionada en trigger.dev.409410| Aspecto | Cloud | Self-hosted |411|--------|-------|-------------|412| **Setup** | Registrarse, ejecutar `init` | Ejecutar docker-compose o Helm chart |413| **Scaling** | Automatico | Tu responsabilidad |414| **Pricing** | Por run + por compute | Solo costo de infra |415| **Compliance** | SOC 2 | Lo que provee tu entorno |416| **Ideal para** | La mayoria de los equipos | Residencia de datos estricta, infra custom |417418El SDK y la CLI son identicos entre modos - cambias un flag de perfil y apuntas a tu propia instancia.419420## Best Practices421422### 1. Manten los payloads pequenos y serializables423424Pasa IDs y referencias, no objetos completos. Recupera los datos dentro de la task. Esto mantiene la cola pequena, los payloads baratos de loggear y te deja cambiar la fuente de datos sin retriggerear.425426### 2. Idempotency keys en cada llamada externa427428Combina `idempotencyKey` en el trigger de la task con idempotency keys en las APIs de tus vendors (Stripe, OpenAI, etc.). Los retries seran seguros end-to-end.429430### 3. Usa `triggerAndWait` para orquestacion, no `Promise.all` de triggers431432Un padre que llama a `triggerAndWait` compone duraderamente las tasks hijos. Un padre que triggea y resuelve inmediatamente pierde la observabilidad de la cadena.433434### 4. Tagea los runs435436Anade `tags` a los triggers (`tags: ["user:123", "feature:onboarding"]`) para poder filtrar el dashboard y la management API por dimensiones de negocio.437438### 5. Manten `init` idempotente439440Corre en cada cold start. Evita migraciones o efectos secundarios one-shot ahi.441442## Conclusion443444Trigger.dev elimina las categorias de trabajo que solian requerir construir un sistema de jobs desde cero. Escribes TypeScript async, lo llamas desde cualquier lugar y la plataforma te da ejecucion duradera, scheduling, colas, retries, actualizaciones en tiempo real y patrones human-in-the-loop out of the box.445446La misma superficie que alimenta un cron nocturno es la superficie que alimenta un agente IA multi-paso que streamea al frontend y pausa para revision. Esa convergencia es lo que hace al framework digno de una mirada seria en 2026, ya sea que estes manejando un SaaS que necesita trabajo en segundo plano confiable o enviando features de IA que sobreviven a un timeout serverless.447448> **Checklist para empezar:**449>450> - [x] Registrate en trigger.dev o ejecuta el stack Docker self-hosted451> - [x] `npx trigger.dev@latest init` en tu proyecto452> - [x] Define tu primera task con `task({ id, run })`453> - [x] Triggeala desde tu API y mira el run en el dashboard454> - [x] Anade `idempotencyKey` y `concurrencyKey` para la seguridad en produccion455> - [x] Conecta `useRealtimeRun` en un componente de status456> - [x] Deploya con `trigger.dev deploy --env prod` desde CI457
:Trigger.dev: Trabajos en segundo plano duraderos y workflows de IA en TypeScriptlines 1-457 (END) — press q to close