inngest/inngest-skills

inngest-steps

Use when implementing delays that must survive process restarts (e.g., 24-hour cart abandonment, scheduled follow-ups), waiting for human approval or external events with timeouts (review gates, webhook callbacks, async API completion), polling external ser…

Quelltext ansehen
Originales Skill-Dokument

Aus dem Quell-Repository gerendert; Überschriften, Beispiele, Code, Tabellen, Links und Bilder bleiben erhalten.

Inngest Steps

Build robust, durable workflows with Inngest's step methods. Each step is a separate HTTP request that can be independently retried and monitored.

These skills are focused on TypeScript. For Python or Go, refer to the Inngest documentation for language-specific guidance. Core concepts apply across all languages.

Core Concept

🔄 Critical: Each step re-runs your function from the beginning. Put ALL non-deterministic code (API calls, DB queries, randomness) inside steps, never outside.

📊 Step Limits: Every function has a maximum of 1,000 steps and 4MB total step data.

typescript
// ❌ 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()

Execute retriable code as a step. Each step ID can be reused - Inngest automatically handles counters.

typescript
// 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!");
});

✅ DO:

  • Put ALL non-deterministic logic inside steps
  • Return useful data for subsequent steps
  • Reuse step IDs in loops (counters handled automatically)

❌ DON'T:

  • Put deterministic logic in steps unnecessarily
  • Forget that each step = separate HTTP request

step.sleep()

Pause execution without using compute time.

typescript
// 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()

Sleep until a specific datetime.

typescript
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()

🚨 CRITICAL: waitForEvent ONLY catches events sent AFTER this step executes.

  • ❌ Event sent before waitForEvent runs → will NOT be caught
  • ✅ Event sent after waitForEvent runs → will be caught
  • Always check for null return (means timeout, event never arrived)
typescript
// 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();
  });
}

✅ DO:

  • Use unique IDs for matching (userId, sessionId, requestId)
  • Always set reasonable timeouts
  • Handle null return (timeout case)
  • Use with Realtime for human-in-the-loop flows

❌ DON'T:

  • Expect events sent before this step to be handled
  • Use without timeouts in production

Expression Syntax

In expressions, event = the original triggering event, async = the new event being matched. See Expression Syntax Reference for full syntax, operators, and patterns.

step.waitForSignal()

Wait for unique signals (not events). Better for 1:1 matching.

typescript
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

When to use:

  • waitForEvent: Multiple functions might handle the same event
  • waitForSignal: Exact 1:1 signal to specific function run

step.sendEvent()

Fan out to other functions without waiting for results.

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

Use when: You want to trigger other functions but don't need their results in the current function.

step.invoke()

Call other functions and handle their results. Perfect for composition.

typescript
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" }
});

Warning: v4 Breaking Change: String function IDs (e.g., function: "my-app-other-fn") are no longer supported in step.invoke(). Use an imported function reference or referenceFunction() for cross-app calls.

Great for:

  • Breaking complex workflows into composable functions
  • Reusing logic across multiple workflows
  • Map-reduce patterns

Patterns

Loops with Steps

Reuse step IDs - Inngest handles counters automatically.

typescript
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);
});

Parallel Execution

Use Promise.all for parallel steps. In v4, parallel step execution is optimized by default

typescript
// 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: [...] }

See inngest-flow-control for concurrency and throttling options.

Chunking Jobs

Perfect for batch processing with parallel steps.

typescript
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);
    });
  }
);

Key Gotchas

🔄 Function Re-execution: Code outside steps runs on every step execution ⏰ Event Timing: waitForEvent only catches events sent AFTER the step runs 🔢 Step Limits: Max 1,000 steps per function, 4MB per step output, 32MB per function run in total 📨 HTTP Requests: Checkpointing is enabled by default in v4, reducing HTTP overhead. For serverless platforms, configure maxRuntime on the client 🔁 Step IDs: Can be reused in loops - Inngest handles counters ⚡ Parallelism: Use Promise.all for parallel steps (optimized by default in v4). Note that Promise.race() waits for all promises to settle — use group.parallel() for true race semantics

Common Use Cases

  • Human-in-the-loop: waitForEvent + Realtime UI
  • Multi-step onboarding: sleep between steps, waitForEvent for user actions
  • Data processing: Parallel steps for chunked work
  • External integrations: step.run for reliable API calls
  • AI workflows: step.ai for durable LLM orchestration
  • Function composition: step.invoke to build complex workflows

Remember: Steps make your functions durable, observable, and debuggable. Embrace them!

aus demselben Repository

Weitere Skills

Alle Skills
inngest
Community

inngest-api-cli

- Use when operating Inngest API resources from the terminal with npx inngest-cli@latest api: Cloud/local run debugging, event-run lookup, function traces, function invocation, app syncs, webhooks, environments, keys, account checks, and Insights queries. Provides prescriptive command routing for agents: which CLI command to run for a run ID, event ID, app ID, function ID, Cloud environment, API key, missing ID, or potentially mutating operation. Use inngest-cli for dev server setup/general CLI workflows and inngest-api only when raw REST API v2 docs or OpenAPI fallback are needed.

Installationen
2
GitHub Stars
28
Aktualisiert
2. Juli
inngest
Community

inngest-cli

Use when installing or running the Inngest CLI and Dev Server for local development, local testing, serve endpoint debugging, Docker or Docker Compose setup, MCP configuration, self-hosted inngest start, or deployment workflow checks. Covers inngest dev, inngest start, auto-discovery, config files, environment variables, @inngest/test, local event sending, platform gotchas, and production/self-hosted server flags.

Installationen
2
GitHub Stars
28
Aktualisiert
2. Juli
inngest
Community

inngest-durable-functions

Use when building functions that must survive process crashes, retry automatically on failure, run on a schedule, react to events, or maintain state across infrastructure failures — e.g., webhook handlers that drop events, flaky cron jobs, background jobs that fail mid-execution, or workflows that need to resume where they left off. Covers Inngest function configuration, triggers (events, cron, invoke), step execution and memoization, idempotency, cancellation, error handling, retries, logging, and observability.

Installationen
2
GitHub Stars
28
Aktualisiert
2. Juli
inngest
Community

inngest-events

Use when designing event-driven workflows, decoupling services, implementing fan-out patterns (one trigger, many downstream handlers), implementing idempotent event handling with IDs (24-hour dedupe window), or handling at-least-once delivery from external sources like Stripe webhooks. Covers Inngest event schema, payload format, naming conventions, IDs for idempotency, the ts param, fan-out patterns, and system events like inngest/function.failed.

Installationen
2
GitHub Stars
28
Aktualisiert
2. Juli