inngest-steps について
TypeScript で Inngest のワークフローを構築する開発者向けに、再試行・監視可能なステップと、待機、並列実行、関数合成の方法を解説します。
主な機能
- 01再試行可能な step.run()
- 02時間指定の一時停止とイベント・シグナル待機
- 03イベント送信と関数呼び出し
- 04並列ステップとチャンク処理
- 05ステップ上限と再実行時の注意点
活用シーン
- 01承認待ちを含むワークフローの構築
- 02外部 API 呼び出しの確実な実行
- 03大規模データセットの並列処理
- 04再利用可能なワークフロー関数の合成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ joelhooks/ joelclaw --skill inngest-stepsインストール後、Claude Code で/inngest-stepsを入力して読み込まれたか確認します。
詳細
Inngest のステップ
Inngest のステップメソッドを使って、堅牢で永続性のあるワークフローを構築します。各ステップは個別の HTTP リクエストであり、それぞれ独立して再試行および監視できます。
これらのスキルは TypeScript に特化しています。 Python または Go については、言語固有のガイダンスが記載された Inngest のドキュメントを参照してください。基本的な概念はすべての言語に適用されます。
基本概念
🔄 重要:各ステップでは、関数が最初から再実行されます。 非決定的なコード(API 呼び出し、DB クエリ、乱数生成)はすべてステップ内に配置し、ステップ外には配置しないでください。
📊 ステップの上限: 各関数のステップ数は最大 1,000、ステップデータの合計は最大 4MB です。
// ❌ WRONG - will run 4 times
export default inngest.createFunction(
{ id: "bad-example" },
{ event: "test" },
async ({ step }) => {
console.log("This logs 4 times!"); // Outside step = bad
await step.run("a", () => console.log("a"));
await step.run("b", () => console.log("b"));
await step.run("c", () => console.log("c"));
}
);
// ✅ CORRECT - logs once each
export default inngest.createFunction(
{ id: "good-example" },
{ event: "test" },
async ({ step }) => {
await step.run("log-hello", () => console.log("hello"));
await step.run("a", () => console.log("a"));
await step.run("b", () => console.log("b"));
await step.run("c", () => console.log("c"));
}
);
step.run()
再試行可能なコードをステップとして実行します。各ステップ ID は再利用できます。カウンターは Inngest が自動的に処理します。
// Basic usage
const result = await step.run("fetch-user", async () => {
const user = await db.user.findById(userId);
return user; // Always return useful data
});
// Synchronous code works too
const transformed = await step.run("transform-data", () => {
return processData(result);
});
// Side effects (no return needed)
await step.run("send-notification", async () => {
await sendEmail(user.email, "Welcome!");
});
✅ 推奨事項:
- 非決定的なロジックはすべてステップ内に配置する
- 後続のステップで役立つデータを返す
- ループ内でステップ ID を再利用する(カウンターは自動的に処理されます)
❌ 避けること:
- 決定的なロジックを不必要にステップ内に配置する
- 各ステップが個別の HTTP リクエストであることを忘れる
step.sleep()
コンピューティング時間を消費せずに実行を一時停止します。
// Duration strings
await step.sleep("wait-24h", "24h");
await step.sleep("short-delay", "30s");
await step.sleep("weekly-pause", "7d");
// Use in workflows
await step.run("send-welcome", () => sendEmail(email));
await step.sleep("wait-for-engagement", "3d");
await step.run("send-followup", () => sendFollowupEmail(email));
step.sleepUntil()
指定した日時までスリープします。
const reminderDate = new Date("2024-12-25T09:00:00Z");
await step.sleepUntil("wait-for-christmas", reminderDate);
// From event data
const scheduledTime = new Date(event.data.remind_at);
await step.sleepUntil("wait-for-scheduled-time", scheduledTime);
step.waitForEvent()
🚨 重要:waitForEvent が捕捉するのは、このステップの実行後に送信されたイベントのみです。
- ❌ waitForEvent の実行前に送信されたイベント → 捕捉されません
- ✅ waitForEvent の実行後に送信されたイベント → 捕捉されます
nullが返された場合(タイムアウトし、イベントが届かなかったことを示します)は、必ず確認してください
// Basic event waiting with timeout
const approval = await step.waitForEvent("wait-for-approval", {
event: "app/invoice.approved",
timeout: "7d",
match: "data.invoiceId" // Simple matching
});
// Expression-based matching (CEL syntax)
const subscription = await step.waitForEvent("wait-for-subscription", {
event: "app/subscription.created",
timeout: "30d",
if: "event.data.userId == async.data.userId && async.data.plan == 'pro'"
});
// Handle timeout
if (!approval) {
await step.run("handle-timeout", () => {
// Approval never came
return notifyAccountingTeam();
});
}
✅ 推奨事項:
- 一致判定には一意の ID(userId、sessionId、requestId)を使用する
- 適切なタイムアウトを必ず設定する
nullが返された場合(タイムアウト時)の処理を行う- 人間が介在するワークフローでは Realtime と併用する
❌ 避けること:
- このステップより前に送信されたイベントも処理されると想定する
- 本番環境でタイムアウトを設定せずに使用する
式の構文
式では、event は元のトリガーイベント、async は一致判定の対象となる新しいイベントです。構文、演算子、パターンの詳細は、式の構文リファレンスを参照してください。
step.waitForSignal()
イベントではなく、一意のシグナルを待機します。1 対 1 の一致判定に適しています。
const taskId = "task-" + crypto.randomUUID();
const signal = await step.waitForSignal("wait-for-task-completion", {
signal: taskId,
timeout: "1h",
onConflict: "replace" // Required: "replace" overwrites pending signal, "fail" throws an error
});
// Send signal elsewhere via Inngest API or SDK
// POST /v1/events with signal matching taskId
使い分け:
- waitForEvent:複数の関数が同じイベントを処理する可能性がある場合
- waitForSignal:特定の関数実行に対して、正確に 1 対 1 でシグナルを送る場合
step.sendEvent()
結果を待たずに、他の関数に処理を振り分けます。
// Trigger other functions
await step.sendEvent("notify-systems", {
name: "user/profile.updated",
data: { userId: user.id, changes: profileChanges }
});
// Multiple events at once
await step.sendEvent("batch-notifications", [
{ name: "billing/invoice.created", data: { invoiceId } },
{ name: "email/invoice.send", data: { email: user.email, invoiceId } }
]);
使用する場合: 他の関数をトリガーしたいものの、現在の関数でその結果が不要な場合に使用します。
step.invoke()
他の関数を呼び出し、その結果を処理します。処理の合成に最適です。
const computeSquare = inngest.createFunction(
{ id: "compute-square" },
{ event: "calculate/square" },
async ({ event }) => {
return { result: event.data.number * event.data.number };
}
);
// Invoke and use result
const square = await step.invoke("get-square", {
function: computeSquare,
data: { number: 4 }
});
console.log(square.result); // 16, fully typed!
次の用途に最適です:
- 複雑なワークフローを、組み合わせ可能な関数に分割する
- 複数のワークフローでロジックを再利用する
- マップリデュースパターン
パターン
ステップを使ったループ
ステップ ID を再利用します。カウンターは Inngest が自動的に処理します。
const allProducts = [];
let cursor = null;
let hasMore = true;
while (hasMore) {
// Same ID "fetch-page" reused - counters handled automatically
const page = await step.run("fetch-page", async () => {
return shopify.products.list({ cursor, limit: 50 });
});
allProducts.push(...page.products);
if (page.products.length < 50) {
hasMore = false;
} else {
cursor = page.products[49].id;
}
}
await step.run("process-products", () => {
return processAllProducts(allProducts);
});
並列実行
並列ステップには Promise.all を使用します。
// Create steps without awaiting
const sendEmail = step.run("send-email", async () => {
return await sendWelcomeEmail(user.email);
});
const updateCRM = step.run("update-crm", async () => {
return await crmService.addUser(user);
});
const createSubscription = step.run("create-subscription", async () => {
return await subscriptionService.create(user.id);
});
// Run all in parallel
const [emailId, crmRecord, subscription] = await Promise.all([
sendEmail,
updateCRM,
createSubscription
]);
// Optimization: Enable optimizeParallelism for many parallel steps
export default inngest.createFunction(
{
id: "parallel-heavy-function",
optimizeParallelism: true // Reduces HTTP requests by ~50%
},
{ event: "process/batch" },
async ({ event, step }) => {
const results = await Promise.all(
event.data.items.map((item, i) =>
step.run(`process-item-${i}`, () => processItem(item))
)
);
}
);
同時実行やスロットリングのオプションについては、inngest-flow-control を参照してください。
ジョブの分割
並列ステップを使ったバッチ処理に最適です。
export default inngest.createFunction(
{ id: "process-large-dataset" },
{ event: "data/process.large" },
async ({ event, step }) => {
const chunks = chunkArray(event.data.items, 10);
// Process chunks in parallel
const results = await Promise.all(
chunks.map((chunk, index) =>
step.run(`process-chunk-${index}`, () => processChunk(chunk))
)
);
// Combine results
await step.run("combine-results", () => {
return aggregateResults(results);
});
}
);
主な注意点
🔄 関数の再実行: ステップ外のコードは、ステップの実行ごとに実行されます
⏰ イベントのタイミング: waitForEvent が捕捉するのは、ステップの実行後に送信されたイベントのみです
🔢 ステップの上限: 関数あたり最大 1,000 ステップ、ステップの出力あたり 4MB、関数の実行全体で 32MB
📨 HTTP リクエスト: serve を使う場合は、HTTP リクエストを減らすために checkpointing を使用します
🔁 ステップ ID: ループ内で再利用できます。カウンターは Inngest が処理します
⚡ 並列処理: Promise.all を使用し、ステップ数が多い場合は optimizeParallelism の利用を検討してください
一般的なユースケース
- 人間が介在するワークフロー: waitForEvent + Realtime UI
- 複数ステップのオンボーディング: ステップ間で sleep を使用し、ユーザーの操作には waitForEvent を使用します
- データ処理: チャンクに分割した作業に並列ステップを使用します
- 外部連携: 信頼性の高い API 呼び出しには step.run を使用します
- AI ワークフロー: 永続性のある LLM オーケストレーションには step.ai を使用します
- 関数の合成: 複雑なワークフローの構築には step.invoke を使用します
ステップを使うと、関数の永続性、可観測性、デバッグのしやすさが向上することを忘れないでください。ぜひ活用しましょう!