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