spinny:~/writing $ vim trigger-dev-background-jobs-guide.md
1~2ほとんどの本番アプリケーションには、リクエスト/レスポンスサイクルに収まらない作業が必要です:メールの送信、アップロードの処理、AI パイプラインの実行、サードパーティデータの同期、レポートの生成。従来の答えは、キュー(Redis、SQS、RabbitMQ)、ワーカーフリート、スケジューラー、そして毎回のデプロイで壊れる脆い接着コードの山です。3~4[Trigger.dev](https://trigger.dev) は、そのスタックを単一の TypeScript SDK に折りたたみます。関数を書き、どこからでも呼び出すと、プラットフォームがキューイング、リトライ、可観測性、スケジューリング、永続実行を処理します。タスクは必要なだけ実行されます - 10 秒のサーバーレスタイムアウトはなく、再デプロイで作業が失われることもありません。5~6## なぜ Trigger.dev なのか7~82026 年のシフトは永続実行です。ワークフローは、再起動、クラッシュ、デプロイ、レート制限を生き延びる必要があります。また、進捗をリアルタイムで UI にストリーミングし、人間の入力のために一時停止する必要があります。Trigger.dev はバージョン 3 でこれらの要件を中心に再構築され、AI インフラストラクチャの表面を拡大し続けています。9~10```mermaid11graph LR12 App[あなたのアプリ] -->|trigger| API[Trigger.dev API]13 API --> Queue[永続キュー]14 Queue --> Worker[ワーカーコンテナ]15 Worker -->|run task| Task[あなたのタスクコード]16 Task -->|metadata| Realtime[リアルタイムストリーム]17 Realtime --> UI[React UI]18 Worker --> Storage[Run 状態ストア]19```20~21モデルはシンプルです:タスクをエクスポートとして定義し、SDK がそれらを取得し、プラットフォームがそれらを隔離されたコンテナでスケジュールして実行し、Run 状態が永続化されるため、再開、リトライ、観察ができます。22~23## はじめる24~25### プロジェクトの初期化26~27```bash28npx trigger.dev@latest login29npx trigger.dev@latest init30```31~32これにより、`trigger.config.ts` ファイルとサンプルタスクを含む `trigger/` ディレクトリが作成されます。設定ファイルはプロジェクトの真実のソースです:どのディレクトリにタスクが含まれるか、ビルド設定、ライフサイクルフック、ランタイムオプション。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### タスクをローカルで実行57~58```bash59npx trigger.dev@latest dev60```61~62dev サーバーはクラウドに接続し、タスクを登録し、Run をローカルコードを通じてストリーミングします。エディタにブレークポイントを設定し、実際のトリガーでヒットします - 通常の Node.js プロジェクトで使用するのと同じループです。63~64## タスクの定義65~66タスクは、一意の `id` と `run` 関数を持つエクスポートされたオブジェクトです。SDK は `dirs` 全体のエクスポートを検査し、自動的に登録します。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~973 つの注目点:98~991. **run ボディにタイムアウトはありません。** プラットフォームは、ランタイムではなく設定の `maxDuration` を介して実行時間を管理します。1002. **Throw はリトライです。** SDK は例外をキャッチし、`retry` ポリシーに従って指数バックオフで再実行します。1013. **戻り値は永続化されます。** 他のタスクとフロントエンドはどこからでも `run.output` を読み取れます。102~103## タスクのトリガー104~105バックエンド、API ルート、または別のタスクからタスクを呼び出します。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 - 進捗を追跡または表示するために使用122```123~124オプションは 1 回の呼び出しで多くの動作を解放します:125~126- **`idempotencyKey`** - 同じキーの Run がすでに存在する場合、SDK は作業を複製する代わりに既存のハンドルを返します。127- **`concurrencyKey`** - キーを共有する Run をシリアライズし、テナントごとのレート制限を超えないようにします。128- **`queue.concurrencyLimit`** - すべてのキー全体でのキューのグローバル上限。129- **`delay`** - Run を将来の時間にスケジュールします。130- **`ttl`** - Run がそれまでに開始されていない場合、自動的に期限切れにします。131~132### バッチトリガー133~134ファンアウトワークロードの場合、`batchTrigger` は呼び出しごとに最大 500 アイテムを受け取り、アイテムごとに 1 つの Run を作成します。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## スケジュールされたタスク146~147Cron ジョブは第一級の宣言になります。スケジュール自体は別のオブジェクトで、タスクに何度も添付できます。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~167テナントごとのスケジュール(たとえば、顧客ごとに 1 つの cron)の場合、管理 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~181`deduplicationKey` は呼び出しを冪等にします:デプロイ時に同じコードを再実行しても、重複したスケジュールは積み重なりません。182~183## キュー、並行性、冪等性184~1853 つのプリミティブが、ほとんどのレート制限と順序付けのニーズをカバーします。186~187```mermaid188graph TB189 Trigger[trigger payload] --> IK{idempotencyKey<br/>確認済み?}190 IK -->|はい| Reuse[既存の run を返す]191 IK -->|いいえ| CK[concurrencyKey バケット]192 CK --> Q[concurrencyLimit を持つ<br/>キュー]193 Q -->|スロット利用可能| Run[task を実行]194 Q -->|スロット満杯| Wait[キューで待機]195```196~197一般的なパターン:ベンダーのレート制限を尊重するために、テナントごとに小さなキーごとの並行性を持つキューと、リトライを安全にするための冪等性キー。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## 待機と長時間実行作業211~212タスクは、接続を保持したり計算を消費したりせずに一時停止できます。プラットフォームは状態を永続化し、待機が完了したときに関数を再開します。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` はキラー機能です:子タスクをトリガーし、子が完了するまで親を一時停止します。タスクを async 関数のように構成しますが、オーケストレーションは数日または数週間にわたって永続的に実行されます。230~231### `wait.forToken` による Human-in-the-loop232~233承認フローと AI ゲートのために、`wait.forToken` は、アプリケーションが結果でコールバックするまで一時停止します。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~258エディタは UI を開き、ドラフトをレビューし、承認をクリックすると、バックエンドがトークンを完了します。タスクは中断したところから再開します - 数時間または数日が経過していても。259~260## ライフサイクルフック261~262`init`、`onStart`、`onSuccess`、`onFailure` をタスクに、またはグローバルに `trigger.config.ts` に添付できます。トレース、エラー報告、共有設定にこれらを使用します。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` はワーカーコンテナごとに起動時に 1 回実行され、Run ごとには実行されないため、クライアントとプールをセットアップする適切な場所です。280~281## フロントエンドのリアルタイム282~283Trigger.dev は、Run 状態の変化(ステータス、メタデータ、出力)をストリーミング API で公開します。React フックはそのストリームをサブスクライブし、自動的に再レンダリングします。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~336サーバー側で特定の Run にスコープを設定したパブリックアクセストークンを生成し、クライアントに送信します。フックは認証、再接続、増分更新を処理します。337~338ワンショットでトリガーとサブスクライブの場合: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## AI エージェントとストリーミング354~355Trigger.dev は AI エージェントの人気のあるランタイムになりました。同じプリミティブ(永続実行、リトライ、待機、リアルタイムメタデータ、human-in-the-loop)が、エージェントが必要とするものとまったく同じだからです。Run の進行中にモデルプロバイダーからのトークンを `metadata` にストリーミングし、フロントエンドはそれらをライブでレンダリングし、Run はサーバーレスタイムアウトを消費せずに長時間実行のツール呼び出しを生き延びます。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~384フロントエンドは `useRealtimeRun` を使用し、`run.metadata.partial` を読み取ってストリーミング応答をレンダリングします。チャット完了をレンダリングするのと同じ方法ですが、これはページのフルリロードを生き延びます。385~386## デプロイ387~388デプロイはタスクをバージョン管理されたバンドルにコンパイルし、コンテナをビルドし、トラフィックを原子的に切り替えます。古い進行中の Run は以前のバージョンを使用し続けます。389~390```bash391npx trigger.dev@latest deploy --env prod392```393~394CI では通常、これをアプリを出荷するのと同じワークフローに配線します: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~404プレビュー環境の場合、`--env preview --branch ${{ github.head_ref }}` を渡すと、Trigger.dev はブランチごとに分離された環境を作成し、Vercel がプレビューデプロイメントを処理する方法を反映します。405~406## セルフホスティング vs クラウド407~408Trigger.dev は Apache 2.0 ライセンスの下でオープンソースです。任意のコンテナプラットフォーム(Docker Compose、Kubernetes、Fly.io)でセルフホスティングするか、trigger.dev でマネージドクラウドを使用できます。409~410| 側面 | クラウド | セルフホスト |411|--------|-------|-------------|412| **セットアップ** | サインアップ、`init` を実行 | docker-compose または Helm chart を実行 |413| **スケーリング** | 自動 | あなたの責任 |414| **価格** | Run ごと + 計算ごと | インフラコストのみ |415| **コンプライアンス** | SOC 2 | 環境が提供するもの |416| **最適** | ほとんどのチーム | 厳格なデータ常駐性、カスタムインフラ |417~418SDK と CLI はモード間で同一です - プロファイルフラグを変更し、自分のインスタンスにポイントします。419~420## ベストプラクティス421~422### 1. ペイロードを小さくシリアライズ可能に保つ423~424完全なオブジェクトではなく、ID と参照を渡します。タスク内でデータを取得します。これにより、キューが小さく保たれ、ペイロードのログが安価になり、再トリガーせずにデータソースを変更できます。425~426### 2. すべての外部呼び出しに冪等性キー427~428タスクトリガーの `idempotencyKey` をベンダー API(Stripe、OpenAI など)の冪等性キーと組み合わせます。リトライはエンドツーエンドで安全になります。429~430### 3. オーケストレーションには `triggerAndWait` を使用し、トリガーの `Promise.all` ではない431~432`triggerAndWait` を呼び出す親は、子タスクを永続的に構成します。トリガーしてすぐに解決する親は、チェーンの可観測性を失います。433~434### 4. Run にタグを付ける435~436トリガーに `tags` を追加(`tags: ["user:123", "feature:onboarding"]`)すると、ダッシュボードと管理 API をビジネスディメンションでフィルタリングできます。437~438### 5. `init` を冪等に保つ439~440コールドスタートのたびに実行されます。マイグレーションやワンショットの副作用をそこに入れないでください。441~442## 結論443~444Trigger.dev は、以前はジョブシステムをゼロから構築する必要があった作業のカテゴリを排除します。async TypeScript を書き、どこからでも呼び出すと、プラットフォームは永続実行、スケジューリング、キュー、リトライ、リアルタイム更新、human-in-the-loop パターンをすぐに提供します。445~446夜間 cron を駆動するのと同じ表面が、フロントエンドにストリーミングしてレビューのために一時停止する複数ステップの AI エージェントを駆動する表面です。その融合が、信頼性の高いバックグラウンド作業を必要とする SaaS を運営しているか、サーバーレスタイムアウトを超えて存続する AI 機能を出荷しているかにかかわらず、フレームワークが 2026 年に真剣に検討する価値があるものにします。447~448> **入門チェックリスト:**449>450> - [x] trigger.dev でサインアップするか、セルフホスト Docker スタックを実行451> - [x] プロジェクトで `npx trigger.dev@latest init`452> - [x] `task({ id, run })` で最初のタスクを定義453> - [x] API からトリガーし、ダッシュボードで Run を見る454> - [x] 本番安全のために `idempotencyKey` と `concurrencyKey` を追加455> - [x] `useRealtimeRun` をステータスコンポーネントに配線456> - [x] CI から `trigger.dev deploy --env prod` でデプロイ457~
NORMAL · trigger-dev-background-jobs-guide.md [readonly]457 lines · :q to close