mcpField
I
inngest-steps

inngest-steps

github.com/inngest开发294.0k 次安装4 周前更新
代码仓库

关于 inngest-steps

面向使用 Inngest 和 TypeScript 的开发者,说明如何用步骤方法构建可持久执行的工作流,涵盖重试、等待、事件协作与函数组合。

主要功能

  1. 01可重试的步骤执行
  2. 02按时长或日期暂停
  3. 03等待事件或唯一信号
  4. 04发送事件并调用其他函数
  5. 05并行步骤与分块处理

适用场景

  1. 01构建分步用户引导流程
  2. 02等待人工审批或用户操作
  3. 03并行处理批量数据
  4. 04组合可复用的工作流函数

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/inngest/inngest-skills --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", triggers: [{ 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", triggers: [{ 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", triggers: [{ 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!

// For cross-app invocation (when you can't import the function directly):
import { referenceFunction } from "inngest";

const externalFn = referenceFunction({
  appId: "other-app",
  functionId: "other-fn"
});

const result = await step.invoke("call-external", {
  function: externalFn,
  data: { key: "value" }
});

警告:v4 重大变更: step.invoke() 不再支持字符串形式的函数 ID(例如 function: "my-app-other-fn")。跨应用调用时,请使用导入的函数引用或 referenceFunction()。

适用于:

  • 将复杂工作流拆分为可组合的函数
  • 在多个工作流之间复用逻辑
  • Map-Reduce 模式

模式

带步骤的循环

重复使用步骤 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 并行执行步骤。在 v4 中,默认已优化并行步骤执行

// 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
]);

// Parallel steps are optimized by default in v4
export default inngest.createFunction(
  {
    id: "parallel-heavy-function",
    triggers: [{ event: "process/batch" }]
  },
  async ({ event, step }) => {
    const results = await Promise.all(
      event.data.items.map((item, i) =>
        step.run(`process-item-${i}`, () => processItem(item))
      )
    );
  }
);

// ⚠️ Promise.race() behavior with v4's optimized parallelism:
// All promises settle before race resolves. Use group.parallel() for true race:
const winner = await group.parallel(async () => {
  return Promise.race([
    step.run("fast-service", () => callFastService()),
    step.run("slow-service", () => callSlowService())
  ]);
});

// To disable optimized parallelism if needed:
// At the client level: new Inngest({ id: "app", optimizeParallelism: false })
// At the function level: { id: "fn", optimizeParallelism: false, triggers: [...] }

有关并发和节流选项,请参阅 inngest-flow-control。

分块处理任务

非常适合使用并行步骤进行批处理。

export default inngest.createFunction(
  { id: "process-large-dataset", triggers: [{ 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 请求: v4 默认启用检查点,可减少 HTTP 开销。对于无服务器平台,请在客户端配置 maxRuntime 🔁 步骤 ID: 可在循环中重复使用——Inngest 会自动处理计数 ⚡ 并行执行: 使用 Promise.all 并行执行步骤(v4 中默认已优化)。注意,Promise.race() 会等待所有 Promise 都结束——如需真正的竞争语义,请使用 group.parallel()

常见用例

  • 人工参与: waitForEvent + Realtime UI
  • 多步引导: 在步骤之间使用 sleep,并通过 waitForEvent 等待用户操作
  • 数据处理: 使用并行步骤处理分块任务
  • 外部集成: 使用 step.run 可靠地调用 API
  • AI 工作流: 使用 step.ai 实现持久可靠的大模型编排
  • 函数组合: 使用 step.invoke 构建复杂的工作流

请记住:步骤能让你的函数具备持久性、可观测性且便于调试。积极使用它们吧!

评论

支持 Markdown