关于 inngest-steps
面向使用 Inngest 和 TypeScript 构建工作流的开发者,介绍可重试、可监控的步骤方法,以及等待、并行执行和函数组合等模式。
主要功能
- 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 调用、数据库查询、随机操作)放入步骤中,绝不要放在步骤之外。
📊 步骤限制: 每个函数最多可包含 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()
等待唯一信号(而非事件)。更适合一对一匹配。
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:将精确的一对一信号发送给特定函数运行
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!
适用场景:
- 将复杂工作流拆分为可组合的函数
- 在多个工作流中复用逻辑
- MapReduce 模式
模式
带步骤的循环
重复使用步骤 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 时,使用 checkpointing 减少 HTTP 请求
🔁 步骤 ID: 可在循环中重复使用——Inngest 会处理计数器
⚡ 并行性: 使用 Promise.all;步骤较多时可考虑 optimizeParallelism
常见用例
- 人工参与: waitForEvent + Realtime UI
- 多步引导: 在步骤之间使用 sleep,并通过 waitForEvent 等待用户操作
- 数据处理: 使用并行步骤处理分块任务
- 外部集成: 使用 step.run 可靠地调用 API
- AI 工作流: 使用 step.ai 实现持久可靠的大模型编排
- 函数组合: 使用 step.invoke 构建复杂的工作流
请记住:步骤能让你的函数具备持久性、可观测性且便于调试。积极使用它们吧!