inngest-events について
Inngest イベントの設計と送信を扱う TypeScript 向けスキルです。ペイロードの形式、命名、重複排除、遅延配信、ファンアウト、システムイベントを解説します。
主な機能
- 01イベントのペイロードと名前の設計
- 02ID による重複排除
- 03イベントの遅延配信
- 04イベントを複数関数へファンアウト
- 05関数のライフサイクルイベントへの対応
活用シーン
- 01一貫したイベントスキーマの定義
- 02イベントの重複処理の防止
- 03リマインダーの後日配信
- 04単一イベントから複数の独立関数を起動
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ inngest/ inngest-skills --skill inngest-eventsインストール後、Claude Code で/inngest-eventsを入力して読み込まれたか確認します。
詳細
Inngest イベント
Inngest のイベント設計と配信パターンをマスターしましょう。イベントは Inngest の基盤です。堅牢なイベントスキーマの設計、冪等性の実装、ファンアウトパターンの活用、システムイベントの効果的な処理を学びます。
これらのスキルは TypeScript に焦点を当てています。 Python または Go については、言語固有のガイダンスとして Inngest ドキュメントを参照してください。コアコンセプトはすべての言語に共通して適用されます。
イベントペイロードの形式
すべての Inngest イベントは、必須プロパティとオプションプロパティを持つ JSON オブジェクトです。
必須プロパティ
type Event = {
name: string; // Event type (triggers functions)
data: object; // Payload data (any nested JSON)
};
完全なスキーマ
type EventPayload = {
name: string; // Required: event type
data: Record<string, any>; // Required: event data
id?: string; // Optional: deduplication ID
ts?: number; // Optional: timestamp (Unix ms)
v?: string; // Optional: schema version
};
基本的なイベントの例
await inngest.send({
name: "billing/invoice.paid",
data: {
customerId: "cus_NffrFeUfNV2Hib",
invoiceId: "in_1J5g2n2eZvKYlo2C0Z1Z2Z3Z",
userId: "user_03028hf09j2d02",
amount: 1000,
metadata: {
accountId: "acct_1J5g2n2eZvKYlo2C0Z1Z2Z3Z",
accountName: "Acme.ai"
}
}
});
イベントの命名規則
Object-Action パターンを使用します: domain/noun.verb
推奨パターン
// ✅ Good: Clear object-action pattern
"billing/invoice.paid";
"user/profile.updated";
"order/item.shipped";
"ai/summary.completed";
// ✅ Good: Domain prefixes for organization
"stripe/customer.created";
"intercom/conversation.assigned";
"slack/message.posted";
// ❌ Avoid: Unclear or inconsistent
"payment"; // What happened?
"user_update"; // Use dots, not underscores
"invoiceWasPaid"; // Too verbose
命名ガイドライン
- 過去形: イベントは何が起きたかを記述します(
created、updated、failed) - ドット記法: 階層にはドットを使用します(
billing/invoice.paid) - プレフィックス: 関連するイベントをグループ化します(
api/user.created、webhook/stripe.received) - 一貫性: パターンを確立し、それを守ります
イベント ID と冪等性
ID を使用する場面: イベントが複数回送信される可能性がある場合に、重複処理を防ぎます。
基本的な重複排除
await inngest.send({
id: "cart-checkout-completed-ed12c8bde", // Unique per event type
name: "storefront/cart.checkout.completed",
data: {
cartId: "ed12c8bde",
items: ["item1", "item2"]
}
});
ID のベストプラクティス
// ✅ Good: Specific to event type and instance
id: `invoice-paid-${invoiceId}`;
id: `user-signup-${userId}-${timestamp}`;
id: `order-shipped-${orderId}-${trackingNumber}`;
// ❌ Bad: Generic IDs shared across event types
id: invoiceId; // Could conflict with other events
id: "user-action"; // Too generic
id: customerId; // Same customer, different events
重複排除ウィンドウ: 最初のイベント受信から 24 時間
冪等性の設定については inngest-durable-functions を参照してください。
遅延配信用の ts パラメータ
使用する場面: 将来の処理のためにイベントをスケジュールする、またはイベントの順序を維持する場合。
将来のスケジューリング
const oneHourFromNow = Date.now() + 60 * 60 * 1000;
await inngest.send({
name: "trial/reminder.send",
ts: oneHourFromNow, // Deliver in 1 hour
data: {
userId: "user_123",
trialExpiresAt: "2024-02-15T12:00:00Z"
}
});
イベント順序の維持
// Events with timestamps are processed in chronological order
const events = [
{
name: "user/action.performed",
ts: 1640995200000, // Earlier
data: { action: "login" }
},
{
name: "user/action.performed",
ts: 1640995260000, // Later
data: { action: "purchase" }
}
];
await inngest.send(events);
ファンアウトパターン
ユースケース: 1 つのイベントが複数の独立した関数をトリガーし、信頼性と並列処理を実現します。
基本的なファンアウトの実装
// Send single event
await inngest.send({
name: "user/signup.completed",
data: {
userId: "user_123",
email: "[email protected]",
plan: "pro"
}
});
// Multiple functions respond to same event
const sendWelcomeEmail = inngest.createFunction(
{ id: "send-welcome-email", triggers: [{ event: "user/signup.completed" }] },
async ({ event, step }) => {
await step.run("send-email", async () => {
return sendEmail({
to: event.data.email,
template: "welcome"
});
});
}
);
const createTrialSubscription = inngest.createFunction(
{ id: "create-trial", triggers: [{ event: "user/signup.completed" }] },
async ({ event, step }) => {
await step.run("create-subscription", async () => {
return stripe.subscriptions.create({
customer: event.data.stripeCustomerId,
trial_period_days: 14
});
});
}
);
const addToCrm = inngest.createFunction(
{ id: "add-to-crm", triggers: [{ event: "user/signup.completed" }] },
async ({ event, step }) => {
await step.run("crm-sync", async () => {
return crm.contacts.create({
email: event.data.email,
plan: event.data.plan
});
});
}
);
ファンアウトの利点
- 独立性: 関数は個別に実行されるため、1 つの失敗が他に影響しません
- 並列実行: すべての関数が同時に実行されます
- 選択的なリプレイ: 失敗した関数のみを再実行できます
- サービス横断: 異なるコードベースや言語の関数をトリガーできます
waitForEvent を使った高度なファンアウト
式の中で、event は元のトリガーイベント、async はマッチングされる新しいイベントを表します。詳細は式構文リファレンスを参照してください。
const orchestrateOnboarding = inngest.createFunction(
{ id: "orchestrate-onboarding", triggers: [{ event: "user/signup.completed" }] },
async ({ event, step }) => {
// Fan out to multiple services
await step.sendEvent("fan-out", [
{ name: "email/welcome.send", data: event.data },
{ name: "subscription/trial.create", data: event.data },
{ name: "crm/contact.add", data: event.data }
]);
// Wait for all to complete
const [emailResult, subResult, crmResult] = await Promise.all([
step.waitForEvent("email-sent", {
event: "email/welcome.sent",
timeout: "5m",
if: `event.data.userId == async.data.userId`
}),
step.waitForEvent("subscription-created", {
event: "subscription/trial.created",
timeout: "5m",
if: `event.data.userId == async.data.userId`
}),
step.waitForEvent("crm-synced", {
event: "crm/contact.added",
timeout: "5m",
if: `event.data.userId == async.data.userId`
})
]);
// Complete onboarding
await step.run("complete-onboarding", async () => {
return completeUserOnboarding(event.data.userId);
});
}
);
step.invoke を含むその他のパターンについては inngest-steps を参照してください。
システムイベント
Inngest は関数のライフサイクル監視のためにシステムイベントを発行します。
利用可能なシステムイベント
// Function execution events
"inngest/function.failed"; // Function failed after retries
"inngest/function.finished"; // Function finished - completed or failed
"inngest/function.cancelled"; // Function cancelled before completion
失敗した関数の処理
const handleFailures = inngest.createFunction(
{ id: "handle-failed-functions", triggers: [{ event: "inngest/function.failed" }] },
async ({ event, step }) => {
const { function_id, run_id, error } = event.data;
await step.run("log-failure", async () => {
logger.error("Function failed", {
functionId: function_id,
runId: run_id,
error: error.message,
stack: error.stack
});
});
// Alert on critical function failures
if (function_id.includes("critical")) {
await step.run("send-alert", async () => {
return alerting.sendAlert({
title: `Critical function failed: ${function_id}`,
severity: "high",
runId: run_id
});
});
}
// Auto-retry certain failures
if (error.code === "RATE_LIMIT_EXCEEDED") {
await step.run("schedule-retry", async () => {
return inngest.send({
name: "retry/function.requested",
ts: Date.now() + 5 * 60 * 1000, // Retry in 5 minutes
data: { originalRunId: run_id }
});
});
}
}
);
イベントの送信
クライアントのセットアップ
// inngest/client.ts
import { Inngest } from "inngest";
export const inngest = new Inngest({
id: "my-app"
});
// You must set INNGEST_EVENT_KEY environment variable in production
単一イベント
const result = await inngest.send({
name: "order/placed",
data: {
orderId: "ord_123",
customerId: "cus_456",
amount: 2500,
items: [
{ id: "item_1", quantity: 2 },
{ id: "item_2", quantity: 1 }
]
}
});
// Returns event IDs for tracking
console.log(result.ids); // ["01HQ8PTAESBZPBDS8JTRZZYY3S"]
バッチイベント
const orderItems = await getOrderItems(orderId);
// Convert to events
const events = orderItems.map((item) => ({
name: "inventory/item.reserved",
data: {
itemId: item.id,
orderId: orderId,
quantity: item.quantity,
warehouseId: item.warehouseId
}
}));
// Send all at once (up to 512kb)
await inngest.send(events);
関数からの送信
inngest.createFunction(
{ id: "process-order", triggers: [{ event: "order/placed" }] },
async ({ event, step }) => {
// Use step.sendEvent() instead of inngest.send() in functions
// for reliability and deduplication
await step.sendEvent("trigger-fulfillment", {
name: "fulfillment/order.received",
data: {
orderId: event.data.orderId,
priority: event.data.customerTier === "premium" ? "high" : "normal"
}
});
}
);
イベント設計のベストプラクティス
スキーマのバージョニング
// Use version field to track schema changes
await inngest.send({
name: "user/profile.updated",
v: "2024-01-15.1", // Schema version
data: {
userId: "user_123",
changes: {
email: "[email protected]",
preferences: { theme: "dark" }
},
// New field in v2 schema
auditInfo: {
changedBy: "user_456",
reason: "user_requested"
}
}
});
リッチなコンテキストデータ
// Include enough context for all consumers
await inngest.send({
name: "payment/charge.succeeded",
data: {
// Primary identifiers
chargeId: "ch_123",
customerId: "cus_456",
// Amount details
amount: 2500,
currency: "usd",
// Context for different consumers
subscription: {
id: "sub_789",
plan: "pro_monthly"
},
invoice: {
id: "inv_012",
number: "INV-2024-001"
},
// Metadata for debugging
paymentMethod: {
type: "card",
last4: "4242",
brand: "visa"
},
metadata: {
source: "stripe_webhook",
environment: "production"
}
}
});
イベント設計の原則:
- 自己完結型: コンシューマーが必要とするすべてのデータを含めます
- 不変: 送信後にイベントスキーマを決して変更しません
- 追跡可能: 相関 ID と監査証跡を含めます
- アクション可能: ビジネスロジックに十分なコンテキストを提供します
- デバッグ可能: トラブルシューティング用のメタデータを含めます