La 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.
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.
Por que Trigger.dev
El 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.
El 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.
Para empezar
Inicializar un proyecto
npx trigger.dev@latest login npx trigger.dev@latest init
Esto 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.
// 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"], });
Ejecutar tasks localmente
npx trigger.dev@latest dev
El 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.
Definir una Task
Una 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.
// 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 }; }, });
Tres cosas a notar:
- Sin timeout en el cuerpo del run. La plataforma gestiona el tiempo de ejecucion via
maxDurationen config, no en el runtime. - Los throws son retries. El SDK captura excepciones y reejecuta con backoff exponencial segun la policy
retry. - El valor de retorno se persiste. Otras tasks y tu frontend pueden leer
run.outputdesde cualquier lugar.
Triggerear Tasks
Llamas a una task desde tu backend, tus rutas API u otra task.
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 - usalo para rastrear o mostrar el progreso
Las opciones desbloquean mucho comportamiento en una sola llamada:
idempotencyKey- si un run con la misma key ya existe, el SDK retorna el handle existente en vez de duplicar trabajo.concurrencyKey- serializa runs que comparten la key para que no excedas un rate limit per-tenant.queue.concurrencyLimit- cap global para la cola a traves de todas las keys.delay- planifica el run para un tiempo futuro.ttl- si el run no ha empezado para entonces, expiralo automaticamente.
Batch trigger
Para workloads de fan-out, batchTrigger acepta hasta 500 items por llamada y crea un run por item.
await sendWelcomeEmail.batchTrigger( newUsers.map((u) => ({ payload: { email: u.email, name: u.name }, options: { idempotencyKey: `welcome-${u.id}` }, })) );
Tasks Programadas
Los cron jobs se vuelven declaraciones de primera clase. El schedule en si es un objeto separado que puedes adjuntar a una task multiples veces.
// 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); }, });
Para schedules per-tenant - digamos, un cron por cliente - los creas dinamicamente a traves de la 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}`, });
La deduplicationKey hace la llamada idempotente: reejecutar el mismo codigo en deploy time no apila schedules duplicados.
Colas, Concurrencia e Idempotencia
Tres primitivas cubren la mayoria de las necesidades de rate-limiting y ordering.
Un 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.
await syncShopifyOrders.trigger( { shopId }, { queue: { name: `shopify-${shopId}`, concurrencyLimit: 2 }, concurrencyKey: shopId, idempotencyKey: `sync-${shopId}-${Date.now() / 60_000 | 0}`, } );
Esperas y Trabajo de Larga Duracion
Las tasks pueden pausarse sin mantener una conexion o quemar compute. La plataforma persiste el estado y reanuda la funcion cuando la espera se completa.
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 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.
Human-in-the-loop con wait.forToken
Para flujos de aprobacion y gates de IA, wait.forToken pausa hasta que tu aplicacion responda con un resultado.
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); }, });
El 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.
Lifecycle Hooks
Puedes adjuntar init, onStart, onSuccess y onFailure a una task o globalmente en trigger.config.ts. Usalos para tracing, error reporting y setup compartido.
// 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 corre una vez por container worker en el boot, no por run, asi que es el lugar correcto para configurar clientes y pools.
Realtime en el Frontend
Trigger.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.
// 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> ); }
Generas 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.
Para trigger-and-subscribe en un solo paso:
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>;
Agentes IA y Streaming
Trigger.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.
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 }; }, });
El 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.
Deployando
Los 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.
npx trigger.dev@latest deploy --env prod
En CI tipicamente conectas esto al mismo workflow que envia tu app:
# .github/workflows/deploy.yml - name: Deploy Trigger.dev env: TRIGGER_ACCESS_TOKEN: ${{ secrets.TRIGGER_ACCESS_TOKEN }} run: npx trigger.dev@latest deploy --env prod
Para 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.
Self-Hosting vs Cloud
Trigger.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.
| Aspecto | Cloud | Self-hosted |
|---|---|---|
| Setup | Registrarse, ejecutar init | Ejecutar docker-compose o Helm chart |
| Scaling | Automatico | Tu responsabilidad |
| Pricing | Por run + por compute | Solo costo de infra |
| Compliance | SOC 2 | Lo que provee tu entorno |
| Ideal para | La mayoria de los equipos | Residencia de datos estricta, infra custom |
El SDK y la CLI son identicos entre modos - cambias un flag de perfil y apuntas a tu propia instancia.
Best Practices
1. Manten los payloads pequenos y serializables
Pasa 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.
2. Idempotency keys en cada llamada externa
Combina 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.
3. Usa triggerAndWait para orquestacion, no Promise.all de triggers
Un padre que llama a triggerAndWait compone duraderamente las tasks hijos. Un padre que triggea y resuelve inmediatamente pierde la observabilidad de la cadena.
4. Tagea los runs
Anade tags a los triggers (tags: ["user:123", "feature:onboarding"]) para poder filtrar el dashboard y la management API por dimensiones de negocio.
5. Manten init idempotente
Corre en cada cold start. Evita migraciones o efectos secundarios one-shot ahi.
Conclusion
Trigger.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.
La 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.
Checklist para empezar:
- Registrate en trigger.dev o ejecuta el stack Docker self-hosted
npx trigger.dev@latest initen tu proyecto- Define tu primera task con
task({ id, run })- Triggeala desde tu API y mira el run en el dashboard
- Anade
idempotencyKeyyconcurrencyKeypara la seguridad en produccion- Conecta
useRealtimeRunen un componente de status- Deploya con
trigger.dev deploy --env proddesde CI