spinny:~/writing $ vim trigger-dev-background-jobs-guide.md
1~2A maioria das aplicacoes em producao precisa de trabalho que nao se encaixa no ciclo requisicao/resposta: enviar emails, processar uploads, executar pipelines de IA, sincronizar dados de terceiros, gerar relatorios. A resposta tradicional e uma fila (Redis, SQS, RabbitMQ), uma frota de workers, um scheduler e uma pilha fragil de codigo de cola que quebra a cada deploy.3~4[Trigger.dev](https://trigger.dev) colapsa essa stack em um unico SDK TypeScript. Voce escreve funcoes, as chama de qualquer lugar e a plataforma gerencia enfileiramento, retries, observabilidade, scheduling e execucao duravel. As tasks rodam pelo tempo necessario - sem timeout serverless de 10 segundos, sem trabalho perdido em redeploys.5~6## Por que Trigger.dev7~8A mudanca em 2026 e a execucao duravel. Os workflows precisam sobreviver a reinicios, crashes, deploys e rate limits. Tambem precisam transmitir o progresso para a UI em tempo real e pausar para input humano. O Trigger.dev foi reconstruido em torno desses requisitos com a versao 3 e continua expandindo sua superficie de infraestrutura para IA.9~10```mermaid11graph LR12 App[Sua App] -->|trigger| API[API Trigger.dev]13 API --> Queue[Fila Duravel]14 Queue --> Worker[Container Worker]15 Worker -->|run task| Task[Codigo da sua Task]16 Task -->|metadata| Realtime[Stream Realtime]17 Realtime --> UI[UI React]18 Worker --> Storage[Store de Estado do Run]19```20~21O modelo e simples: voce define tasks como exports, o SDK as recolhe, a plataforma as agenda e executa em containers isolados e o estado do run e persistido para que voce possa retomar, retentar e observar.22~23## Comecando24~25### Inicializar um projeto26~27```bash28npx trigger.dev@latest login29npx trigger.dev@latest init30```31~32Isso cria um arquivo `trigger.config.ts` e um diretorio `trigger/` com tasks de exemplo. O arquivo de config e a fonte de verdade do seu projeto: quais diretorios contem tasks, configuracoes de build, lifecycle hooks e opcoes de runtime.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### Executar tasks localmente57~58```bash59npx trigger.dev@latest dev60```61~62O servidor de dev se conecta a cloud, registra suas tasks e transmite os runs atraves do seu codigo local. Voce coloca breakpoints no seu editor e os atinge em triggers reais - o mesmo loop que voce usaria em qualquer projeto Node.js normal.63~64## Definindo uma Task65~66Uma task e um objeto exportado com um `id` unico e uma funcao `run`. O SDK inspeciona exports atraves de `dirs` e os registra automaticamente.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~97Tres coisas a notar:98~991. **Sem timeout no corpo do run.** A plataforma gerencia o tempo de execucao via `maxDuration` na config, nao no runtime.1002. **Throws sao retries.** O SDK captura excecoes e re-executa com backoff exponencial conforme a politica `retry`.1013. **O valor de retorno e persistido.** Outras tasks e seu frontend podem ler `run.output` de qualquer lugar.102~103## Triggando Tasks104~105Voce chama uma task do seu backend, suas rotas API ou outra 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 - use para rastrear ou exibir o progresso122```123~124As opcoes desbloqueiam muito comportamento em uma unica chamada:125~126- **`idempotencyKey`** - se um run com a mesma key ja existir, o SDK retorna o handle existente em vez de duplicar trabalho.127- **`concurrencyKey`** - serializa runs que compartilham a key para que voce nao ultrapasse um rate limit por tenant.128- **`queue.concurrencyLimit`** - cap global para a fila atraves de todas as keys.129- **`delay`** - agenda o run para um tempo futuro.130- **`ttl`** - se o run nao tiver iniciado ate la, expira-o automaticamente.131~132### Batch trigger133~134Para workloads de fan-out, `batchTrigger` aceita ate 500 itens por chamada e cria um run por item.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## Tasks Agendadas146~147Cron jobs se tornam declaracoes de primeira classe. O proprio schedule e um objeto separado que voce pode anexar a uma task multiplas vezes.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~167Para schedules por tenant - digamos, um cron por cliente - voce os cria dinamicamente atraves da 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~181A `deduplicationKey` torna a chamada idempotente: re-executar o mesmo codigo no momento do deploy nao empilha schedules duplicados.182~183## Filas, Concorrencia e Idempotencia184~185Tres primitivas cobrem a maioria das necessidades de rate-limiting e ordenacao.186~187```mermaid188graph TB189 Trigger[trigger payload] --> IK{idempotencyKey<br/>vista?}190 IK -->|sim| Reuse[Retornar run existente]191 IK -->|nao| CK[bucket concurrencyKey]192 CK --> Q[Fila com<br/>concurrencyLimit]193 Q -->|slot disponivel| Run[Executar task]194 Q -->|slots cheios| Wait[Esperar na fila]195```196~197Um padrao comum: uma fila por tenant com uma pequena concorrencia por key para respeitar o rate limit de um vendor, mais uma idempotency key para tornar os retries seguros.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## Esperas e Trabalho de Longa Duracao211~212Tasks podem pausar sem manter uma conexao ou queimar compute. A plataforma persiste o estado e retoma a funcao quando a espera se completa.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` e o recurso decisivo: triggera uma task filha e suspende o pai ate que a filha se complete. Voce compoe tasks como funcoes async, mas a orquestracao roda duravelmente atraves de dias ou semanas.230~231### Human-in-the-loop com `wait.forToken`232~233Para fluxos de aprovacao e gates de IA, `wait.forToken` pausa ate que sua aplicacao responda com um resultado.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~258O editor abre uma UI, revisa o rascunho, clica em Aprovar e seu backend completa o token. A task continua de onde parou - mesmo que horas ou dias tenham passado.259~260## Lifecycle Hooks261~262Voce pode anexar `init`, `onStart`, `onSuccess` e `onFailure` a uma task ou globalmente em `trigger.config.ts`. Use-os para tracing, error reporting e setup compartilhado.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` roda uma vez por container worker no boot, nao por run, entao e o lugar certo para configurar clientes e pools.280~281## Realtime no Frontend282~283O Trigger.dev publica mudancas de estado do run - status, metadata, output - sobre uma API em streaming. Os hooks React se inscrevem nesse stream e re-renderizam automaticamente.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~336Voce gera o public access token no servidor, com escopo para um run especifico, e o envia ao cliente. O hook gerencia auth, reconexao e atualizacoes incrementais.337~338Para trigger-and-subscribe em uma so vez: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## Agentes de IA e Streaming354~355O Trigger.dev se tornou um runtime popular para agentes de IA porque as mesmas primitivas - execucao duravel, retries, esperas, metadata em tempo real, human-in-the-loop - sao exatamente o que os agentes precisam. Voce transmite tokens de um provedor de modelo para `metadata` enquanto o run esta acontecendo, o frontend os renderiza ao vivo e o run sobrevive a tool calls de longa duracao sem queimar um timeout serverless.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~384O frontend usa `useRealtimeRun` e le `run.metadata.partial` para renderizar a resposta em streaming, da mesma forma que voce renderizaria uma chat completion - exceto que esta sobrevive a um reload completo da pagina.385~386## Deployando387~388Os deploys compilam suas tasks em um bundle versionado, constroem um container e fazem swap atomico do trafego. Os runs antigos em voo continuam usando a versao anterior.389~390```bash391npx trigger.dev@latest deploy --env prod392```393~394Em CI voce tipicamente conecta isso ao mesmo workflow que envia sua 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~404Para ambientes de preview, passe `--env preview --branch ${{ github.head_ref }}` e o Trigger.dev cria um ambiente isolado por branch, espelhando como o Vercel lida com preview deployments.405~406## Self-Hosting vs Cloud407~408O Trigger.dev e open source sob a licenca Apache 2.0. Voce pode self-hostear em qualquer plataforma de containers (Docker Compose, Kubernetes, Fly.io) ou usar a cloud gerenciada em trigger.dev.409~410| Aspecto | Cloud | Self-hosted |411|--------|-------|-------------|412| **Setup** | Cadastrar, executar `init` | Executar docker-compose ou Helm chart |413| **Scaling** | Automatico | Sua responsabilidade |414| **Pricing** | Por run + por compute | Apenas custo de infra |415| **Compliance** | SOC 2 | O que seu ambiente fornece |416| **Ideal para** | A maioria das equipes | Residencia de dados estrita, infra customizada |417~418O SDK e a CLI sao identicos entre os modos - voce muda uma flag de profile e aponta para sua propria instancia.419~420## Best Practices421~422### 1. Mantenha os payloads pequenos e serializaveis423~424Passe IDs e referencias, nao objetos completos. Recupere os dados dentro da task. Isso mantem a fila pequena, payloads baratos para logar e permite mudar a fonte de dados sem re-triggerar.425~426### 2. Idempotency keys em cada chamada externa427~428Combine `idempotencyKey` no trigger da task com idempotency keys nas APIs dos seus vendors (Stripe, OpenAI, etc.). Os retries serao seguros end-to-end.429~430### 3. Use `triggerAndWait` para orquestracao, nao `Promise.all` de triggers431~432Um pai que chama `triggerAndWait` compoe duravelmente tasks filhas. Um pai que triggera e resolve imediatamente perde a observabilidade da cadeia.433~434### 4. Tagge os runs435~436Adicione `tags` aos triggers (`tags: ["user:123", "feature:onboarding"]`) para poder filtrar o dashboard e a management API por dimensoes de negocio.437~438### 5. Mantenha `init` idempotente439~440Roda em cada cold start. Evite migrations ou efeitos colaterais one-shot la dentro.441~442## Conclusao443~444O Trigger.dev remove as categorias de trabalho que costumavam exigir construir um sistema de jobs do zero. Voce escreve TypeScript async, o chama de qualquer lugar e a plataforma te da execucao duravel, scheduling, filas, retries, atualizacoes em tempo real e padroes human-in-the-loop out of the box.445~446A mesma superficie que alimenta um cron noturno e a superficie que alimenta um agente de IA multi-passo que transmite ao frontend e pausa para revisao. Essa convergencia e o que torna o framework digno de uma olhada seria em 2026, seja voce gerenciando um SaaS que precisa de trabalho em segundo plano confiavel ou enviando recursos de IA que sobrevivem a um timeout serverless.447~448> **Checklist para Comecar:**449>450> - [x] Cadastre-se em trigger.dev ou execute o stack Docker self-hosted451> - [x] `npx trigger.dev@latest init` no seu projeto452> - [x] Defina sua primeira task com `task({ id, run })`453> - [x] Triggere-a da sua API e veja o run no dashboard454> - [x] Adicione `idempotencyKey` e `concurrencyKey` para seguranca em producao455> - [x] Conecte `useRealtimeRun` em um componente de status456> - [x] Deploye com `trigger.dev deploy --env prod` do CI457~
NORMAL · trigger-dev-background-jobs-guide.md [readonly]457 lines · :q to close