spinny:~/writing $ less trigger-dev-background-jobs-guide.md
12Большинству приложений в продакшне нужна работа, которая не вписывается в цикл запрос/ответ: отправка писем, обработка загрузок, запуск AI-пайплайнов, синхронизация сторонних данных, генерация отчётов. Традиционный ответ - очередь (Redis, SQS, RabbitMQ), флот воркеров, планировщик и хрупкая куча связующего кода, который ломается при каждом деплое.34[Trigger.dev](https://trigger.dev) сворачивает этот стек в один TypeScript SDK. Вы пишете функции, вызываете их откуда угодно, а платформа берёт на себя очередь, ретраи, наблюдаемость, планирование и долговечное выполнение. Задачи работают столько, сколько нужно - никакого 10-секундного serverless-таймаута, никакой потерянной работы при редеплое.56## Почему Trigger.dev78Сдвиг 2026 года - долговечное выполнение. Воркфлоу должны переживать перезапуски, краши, деплои и rate limits. Они также должны стримить прогресс в UI в реальном времени и приостанавливаться для ввода человека. Trigger.dev был перестроен вокруг этих требований в версии 3 и продолжает расширять свою AI-инфраструктурную поверхность.910```mermaid11graph LR12 App[Ваше приложение] -->|trigger| API[API Trigger.dev]13 API --> Queue[Долговечная очередь]14 Queue --> Worker[Worker контейнер]15 Worker -->|run task| Task[Код вашей задачи]16 Task -->|metadata| Realtime[Реалтайм-стрим]17 Realtime --> UI[React UI]18 Worker --> Storage[Хранилище состояния Run]19```2021Модель проста: вы определяете задачи как экспорты, SDK их подбирает, платформа планирует и запускает их в изолированных контейнерах, а состояние run сохраняется, чтобы вы могли возобновить, повторить и наблюдать.2223## Начало работы2425### Инициализация проекта2627```bash28npx trigger.dev@latest login29npx trigger.dev@latest init30```3132Это создаёт файл `trigger.config.ts` и каталог `trigger/` с примерами задач. Файл config - источник истины вашего проекта: какие каталоги содержат задачи, настройки сборки, lifecycle hooks и опции рантайма.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### Запуск задач локально5758```bash59npx trigger.dev@latest dev60```6162Dev-сервер подключается к облаку, регистрирует ваши задачи и стримит run через ваш локальный код. Вы ставите breakpoints в редакторе и попадаете в них на реальных триггерах - тот же цикл, что вы использовали бы в любом обычном Node.js-проекте.6364## Определение задачи6566Задача - это объект, экспортируемый с уникальным `id` и функцией `run`. SDK инспектирует экспорты в `dirs` и регистрирует их автоматически.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```9697Три вещи, на которые стоит обратить внимание:98991. **В теле run нет таймаута.** Платформа управляет временем выполнения через `maxDuration` в config, а не в рантайме.1002. **Throws - это ретраи.** SDK ловит исключения и перезапускает с экспоненциальным backoff согласно политике `retry`.1013. **Возвращаемое значение сохраняется.** Другие задачи и ваш фронтенд могут читать `run.output` откуда угодно.102103## Триггер задач104105Вы вызываете задачу из бэкенда, API-роутов или другой задачи.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 - используйте для отслеживания или отображения прогресса122```123124Опции разблокируют много поведения в одном вызове:125126- **`idempotencyKey`** - если run с тем же ключом уже существует, SDK возвращает существующий handle вместо дублирования работы.127- **`concurrencyKey`** - сериализует runs, разделяющие ключ, чтобы вы не превысили per-tenant rate limit.128- **`queue.concurrencyLimit`** - глобальный cap для очереди по всем ключам.129- **`delay`** - планирует run на будущее время.130- **`ttl`** - если run к тому времени не запустился, автоматически истекает.131132### Batch trigger133134Для fan-out нагрузок `batchTrigger` принимает до 500 элементов на вызов и создаёт один run на элемент.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## Запланированные задачи146147Cron-задания становятся декларациями первого класса. Само расписание - это отдельный объект, который можно прикрепить к задаче несколько раз.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```166167Для расписаний на каждого tenant - скажем, один cron на клиента - вы создаёте их динамически через 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```180181`deduplicationKey` делает вызов идемпотентным: повторный запуск того же кода во время деплоя не накапливает дублированные расписания.182183## Очереди, конкурентность и идемпотентность184185Три примитива покрывают большинство потребностей в rate-limiting и упорядочивании.186187```mermaid188graph TB189 Trigger[trigger payload] --> IK{idempotencyKey<br/>видели?}190 IK -->|да| Reuse[Вернуть существующий run]191 IK -->|нет| CK[бакет concurrencyKey]192 CK --> Q[Очередь с<br/>concurrencyLimit]193 Q -->|слот доступен| Run[Запустить task]194 Q -->|слоты заняты| Wait[Ожидание в очереди]195```196197Распространённый паттерн: одна очередь на tenant с малой per-key конкурентностью, чтобы соблюдать rate limit вендора, плюс ключ идемпотентности, чтобы сделать ретраи безопасными.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## Ожидания и долгая работа211212Задачи могут приостанавливаться без удержания соединения или сжигания compute. Платформа сохраняет состояние и возобновляет функцию, когда ожидание завершается.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` - убийственная фича: она триггерит дочернюю задачу и приостанавливает родителя до завершения дочерней. Вы компонуете задачи как async-функции, но оркестрация работает долговечно через дни или недели.230231### Human-in-the-loop с `wait.forToken`232233Для approval-флоу и AI-гейтов `wait.forToken` приостанавливается, пока ваше приложение не вызовет callback с результатом.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```257258Редактор открывает UI, проверяет черновик, нажимает Approve, и ваш бэкенд завершает токен. Задача продолжается с того места, где остановилась - даже если прошли часы или дни.259260## Lifecycle hooks261262Вы можете прикрепить `init`, `onStart`, `onSuccess` и `onFailure` к задаче или глобально в `trigger.config.ts`. Используйте их для трейсинга, error reporting и общего setup.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` запускается один раз на worker-контейнер при загрузке, а не на каждый run, поэтому это правильное место для настройки клиентов и пулов.280281## Реалтайм во фронтенде282283Trigger.dev публикует изменения состояния run - status, metadata, output - через streaming API. React-хуки подписываются на этот поток и автоматически перерендеривают.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```335336Вы генерируете публичный access token на стороне сервера, scoped к конкретному run, и отправляете его клиенту. Хук обрабатывает auth, переподключение и инкрементальные обновления.337338Для trigger-and-subscribe в один шаг: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## AI-агенты и стриминг354355Trigger.dev стал популярным runtime для AI-агентов, потому что те же примитивы - долговечное выполнение, ретраи, ожидания, реалтайм-метаданные, human-in-the-loop - это именно то, что нужно агентам. Вы стримите токены от model provider в `metadata` пока run происходит, фронтенд рендерит их в живую, и run выживает после долгих tool calls без сжигания 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```383384Фронтенд использует `useRealtimeRun` и читает `run.metadata.partial`, чтобы рендерить streaming-ответ, так же как вы рендерили бы chat completion - за исключением того, что этот переживёт полную перезагрузку страницы.385386## Деплой387388Деплои компилируют ваши задачи в версионированный bundle, собирают контейнер и атомарно переключают трафик. Старые in-flight runs продолжают использовать предыдущую версию.389390```bash391npx trigger.dev@latest deploy --env prod392```393394В CI вы обычно подключаете это к тому же воркфлоу, который доставляет ваше приложение: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```403404Для preview-окружений передайте `--env preview --branch ${{ github.head_ref }}` и Trigger.dev создаст изолированное окружение на каждую ветку, отражая то, как Vercel обрабатывает preview deployments.405406## Self-Hosting vs Cloud407408Trigger.dev открыт под лицензией Apache 2.0. Вы можете self-host на любой контейнерной платформе (Docker Compose, Kubernetes, Fly.io) или использовать управляемое облако на trigger.dev.409410| Аспект | Cloud | Self-hosted |411|--------|-------|-------------|412| **Setup** | Регистрация, запуск `init` | Запуск docker-compose или Helm chart |413| **Scaling** | Автоматический | Ваша ответственность |414| **Pricing** | За run + за compute | Только инфра-затраты |415| **Compliance** | SOC 2 | То, что предоставляет ваше окружение |416| **Идеально для** | Большинства команд | Строгого data residency, кастомной инфры |417418SDK и CLI идентичны между режимами - вы меняете флаг профиля и указываете на свой собственный экземпляр.419420## Best Practices421422### 1. Держите payloads маленькими и сериализуемыми423424Передавайте ID и ссылки, а не полные объекты. Подтягивайте данные внутри задачи. Это держит очередь маленькой, payloads дешевы для логгирования и позволяет менять источник данных без перетриггера.425426### 2. Idempotency keys на каждом внешнем вызове427428Комбинируйте `idempotencyKey` на task trigger с idempotency keys на API ваших вендоров (Stripe, OpenAI и т.д.). Ретраи будут безопасны end-to-end.429430### 3. Используйте `triggerAndWait` для оркестрации, а не `Promise.all` из триггеров431432Родитель, который вызывает `triggerAndWait`, долговечно компонует дочерние задачи. Родитель, который триггерит и сразу резолвится, теряет наблюдаемость цепочки.433434### 4. Тегируйте runs435436Добавляйте `tags` к триггерам (`tags: ["user:123", "feature:onboarding"]`), чтобы фильтровать дашборд и management API по бизнес-измерениям.437438### 5. Держите `init` идемпотентным439440Он запускается на каждом cold start. Избегайте миграций или one-shot побочных эффектов там.441442## Заключение443444Trigger.dev убирает категории работы, для которых раньше нужно было строить job-систему с нуля. Вы пишете async TypeScript, вызываете его откуда угодно, и платформа даёт вам долговечное выполнение, планирование, очереди, ретраи, реалтайм-обновления и паттерны human-in-the-loop из коробки.445446Та же поверхность, которая питает ночной cron, - это поверхность, которая питает многошагового AI-агента, стримящего во фронтенд и приостанавливающегося для review. Эта конвергенция и делает фреймворк достойным серьёзного взгляда в 2026 году, независимо от того, управляете ли вы SaaS, которому нужна надёжная фоновая работа, или поставляете AI-фичи, переживающие serverless-таймаут.447448> **Чеклист для начала:**449>450> - [x] Зарегистрируйтесь на trigger.dev или запустите self-hosted Docker-стек451> - [x] `npx trigger.dev@latest init` в вашем проекте452> - [x] Определите свою первую задачу с `task({ id, run })`453> - [x] Триггерните её из вашего API и посмотрите run в дашборде454> - [x] Добавьте `idempotencyKey` и `concurrencyKey` для production-безопасности455> - [x] Подключите `useRealtimeRun` к компоненту статуса456> - [x] Деплойте с `trigger.dev deploy --env prod` из CI457
:Trigger.dev: долговечные фоновые задачи и AI-воркфлоу на TypeScriptlines 1-457 (END) — press q to close