Большинству приложений в продакшне нужна работа, которая не вписывается в цикл запрос/ответ: отправка писем, обработка загрузок, запуск AI-пайплайнов, синхронизация сторонних данных, генерация отчётов. Традиционный ответ - очередь (Redis, SQS, RabbitMQ), флот воркеров, планировщик и хрупкая куча связующего кода, который ломается при каждом деплое.
Trigger.dev сворачивает этот стек в один TypeScript SDK. Вы пишете функции, вызываете их откуда угодно, а платформа берёт на себя очередь, ретраи, наблюдаемость, планирование и долговечное выполнение. Задачи работают столько, сколько нужно - никакого 10-секундного serverless-таймаута, никакой потерянной работы при редеплое.
Почему Trigger.dev
Сдвиг 2026 года - долговечное выполнение. Воркфлоу должны переживать перезапуски, краши, деплои и rate limits. Они также должны стримить прогресс в UI в реальном времени и приостанавливаться для ввода человека. Trigger.dev был перестроен вокруг этих требований в версии 3 и продолжает расширять свою AI-инфраструктурную поверхность.
Модель проста: вы определяете задачи как экспорты, SDK их подбирает, платформа планирует и запускает их в изолированных контейнерах, а состояние run сохраняется, чтобы вы могли возобновить, повторить и наблюдать.
Начало работы
Инициализация проекта
npx trigger.dev@latest login npx trigger.dev@latest init
Это создаёт файл trigger.config.ts и каталог trigger/ с примерами задач. Файл config - источник истины вашего проекта: какие каталоги содержат задачи, настройки сборки, lifecycle hooks и опции рантайма.
// 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"], });
Запуск задач локально
npx trigger.dev@latest dev
Dev-сервер подключается к облаку, регистрирует ваши задачи и стримит run через ваш локальный код. Вы ставите breakpoints в редакторе и попадаете в них на реальных триггерах - тот же цикл, что вы использовали бы в любом обычном Node.js-проекте.
Определение задачи
Задача - это объект, экспортируемый с уникальным id и функцией run. SDK инспектирует экспорты в dirs и регистрирует их автоматически.
// 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 }; }, });
Три вещи, на которые стоит обратить внимание:
- В теле run нет таймаута. Платформа управляет временем выполнения через
maxDurationв config, а не в рантайме. - Throws - это ретраи. SDK ловит исключения и перезапускает с экспоненциальным backoff согласно политике
retry. - Возвращаемое значение сохраняется. Другие задачи и ваш фронтенд могут читать
run.outputоткуда угодно.
Триггер задач
Вы вызываете задачу из бэкенда, API-роутов или другой задачи.
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 - используйте для отслеживания или отображения прогресса
Опции разблокируют много поведения в одном вызове:
idempotencyKey- если run с тем же ключом уже существует, SDK возвращает существующий handle вместо дублирования работы.concurrencyKey- сериализует runs, разделяющие ключ, чтобы вы не превысили per-tenant rate limit.queue.concurrencyLimit- глобальный cap для очереди по всем ключам.delay- планирует run на будущее время.ttl- если run к тому времени не запустился, автоматически истекает.
Batch trigger
Для fan-out нагрузок batchTrigger принимает до 500 элементов на вызов и создаёт один run на элемент.
await sendWelcomeEmail.batchTrigger( newUsers.map((u) => ({ payload: { email: u.email, name: u.name }, options: { idempotencyKey: `welcome-${u.id}` }, })) );
Запланированные задачи
Cron-задания становятся декларациями первого класса. Само расписание - это отдельный объект, который можно прикрепить к задаче несколько раз.
// 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); }, });
Для расписаний на каждого tenant - скажем, один cron на клиента - вы создаёте их динамически через 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}`, });
deduplicationKey делает вызов идемпотентным: повторный запуск того же кода во время деплоя не накапливает дублированные расписания.
Очереди, конкурентность и идемпотентность
Три примитива покрывают большинство потребностей в rate-limiting и упорядочивании.
Распространённый паттерн: одна очередь на tenant с малой per-key конкурентностью, чтобы соблюдать rate limit вендора, плюс ключ идемпотентности, чтобы сделать ретраи безопасными.
await syncShopifyOrders.trigger( { shopId }, { queue: { name: `shopify-${shopId}`, concurrencyLimit: 2 }, concurrencyKey: shopId, idempotencyKey: `sync-${shopId}-${Date.now() / 60_000 | 0}`, } );
Ожидания и долгая работа
Задачи могут приостанавливаться без удержания соединения или сжигания compute. Платформа сохраняет состояние и возобновляет функцию, когда ожидание завершается.
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 - убийственная фича: она триггерит дочернюю задачу и приостанавливает родителя до завершения дочерней. Вы компонуете задачи как async-функции, но оркестрация работает долговечно через дни или недели.
Human-in-the-loop с wait.forToken
Для approval-флоу и AI-гейтов wait.forToken приостанавливается, пока ваше приложение не вызовет callback с результатом.
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); }, });
Редактор открывает UI, проверяет черновик, нажимает Approve, и ваш бэкенд завершает токен. Задача продолжается с того места, где остановилась - даже если прошли часы или дни.
Lifecycle hooks
Вы можете прикрепить init, onStart, onSuccess и onFailure к задаче или глобально в trigger.config.ts. Используйте их для трейсинга, error reporting и общего setup.
// 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 запускается один раз на worker-контейнер при загрузке, а не на каждый run, поэтому это правильное место для настройки клиентов и пулов.
Реалтайм во фронтенде
Trigger.dev публикует изменения состояния run - status, metadata, output - через streaming API. React-хуки подписываются на этот поток и автоматически перерендеривают.
// 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> ); }
Вы генерируете публичный access token на стороне сервера, scoped к конкретному run, и отправляете его клиенту. Хук обрабатывает auth, переподключение и инкрементальные обновления.
Для trigger-and-subscribe в один шаг:
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>;
AI-агенты и стриминг
Trigger.dev стал популярным runtime для AI-агентов, потому что те же примитивы - долговечное выполнение, ретраи, ожидания, реалтайм-метаданные, human-in-the-loop - это именно то, что нужно агентам. Вы стримите токены от model provider в metadata пока run происходит, фронтенд рендерит их в живую, и run выживает после долгих tool calls без сжигания 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 }; }, });
Фронтенд использует useRealtimeRun и читает run.metadata.partial, чтобы рендерить streaming-ответ, так же как вы рендерили бы chat completion - за исключением того, что этот переживёт полную перезагрузку страницы.
Деплой
Деплои компилируют ваши задачи в версионированный bundle, собирают контейнер и атомарно переключают трафик. Старые in-flight runs продолжают использовать предыдущую версию.
npx trigger.dev@latest deploy --env prod
В CI вы обычно подключаете это к тому же воркфлоу, который доставляет ваше приложение:
# .github/workflows/deploy.yml - name: Deploy Trigger.dev env: TRIGGER_ACCESS_TOKEN: ${{ secrets.TRIGGER_ACCESS_TOKEN }} run: npx trigger.dev@latest deploy --env prod
Для preview-окружений передайте --env preview --branch ${{ github.head_ref }} и Trigger.dev создаст изолированное окружение на каждую ветку, отражая то, как Vercel обрабатывает preview deployments.
Self-Hosting vs Cloud
Trigger.dev открыт под лицензией Apache 2.0. Вы можете self-host на любой контейнерной платформе (Docker Compose, Kubernetes, Fly.io) или использовать управляемое облако на trigger.dev.
| Аспект | Cloud | Self-hosted |
|---|---|---|
| Setup | Регистрация, запуск init | Запуск docker-compose или Helm chart |
| Scaling | Автоматический | Ваша ответственность |
| Pricing | За run + за compute | Только инфра-затраты |
| Compliance | SOC 2 | То, что предоставляет ваше окружение |
| Идеально для | Большинства команд | Строгого data residency, кастомной инфры |
SDK и CLI идентичны между режимами - вы меняете флаг профиля и указываете на свой собственный экземпляр.
Best Practices
1. Держите payloads маленькими и сериализуемыми
Передавайте ID и ссылки, а не полные объекты. Подтягивайте данные внутри задачи. Это держит очередь маленькой, payloads дешевы для логгирования и позволяет менять источник данных без перетриггера.
2. Idempotency keys на каждом внешнем вызове
Комбинируйте idempotencyKey на task trigger с idempotency keys на API ваших вендоров (Stripe, OpenAI и т.д.). Ретраи будут безопасны end-to-end.
3. Используйте triggerAndWait для оркестрации, а не Promise.all из триггеров
Родитель, который вызывает triggerAndWait, долговечно компонует дочерние задачи. Родитель, который триггерит и сразу резолвится, теряет наблюдаемость цепочки.
4. Тегируйте runs
Добавляйте tags к триггерам (tags: ["user:123", "feature:onboarding"]), чтобы фильтровать дашборд и management API по бизнес-измерениям.
5. Держите init идемпотентным
Он запускается на каждом cold start. Избегайте миграций или one-shot побочных эффектов там.
Заключение
Trigger.dev убирает категории работы, для которых раньше нужно было строить job-систему с нуля. Вы пишете async TypeScript, вызываете его откуда угодно, и платформа даёт вам долговечное выполнение, планирование, очереди, ретраи, реалтайм-обновления и паттерны human-in-the-loop из коробки.
Та же поверхность, которая питает ночной cron, - это поверхность, которая питает многошагового AI-агента, стримящего во фронтенд и приостанавливающегося для review. Эта конвергенция и делает фреймворк достойным серьёзного взгляда в 2026 году, независимо от того, управляете ли вы SaaS, которому нужна надёжная фоновая работа, или поставляете AI-фичи, переживающие serverless-таймаут.
Чеклист для начала:
- Зарегистрируйтесь на trigger.dev или запустите self-hosted Docker-стек
npx trigger.dev@latest initв вашем проекте- Определите свою первую задачу с
task({ id, run })- Триггерните её из вашего API и посмотрите run в дашборде
- Добавьте
idempotencyKeyиconcurrencyKeyдля production-безопасности- Подключите
useRealtimeRunк компоненту статуса- Деплойте с
trigger.dev deploy --env prodиз CI