Skip to main content
skill CX Codex

inngest-steps — Claude Code skill: what it does, how to install

Teaches your agent how to write durable Inngest workflow steps in TypeScript. Helpful for long pauses, webhook callbacks, and multi-step tasks.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add inngest/inngest-skills --skill inngest-steps -a claude-code
Open on GitHub

What inngest-steps does

The skill guides your agent through Inngest step methods: step.run, step.sleep, step.waitForEvent, step.waitForSignal, step.sendEvent, and step.invoke. Inngest runs each step as an isolated HTTP request. Because of that architecture, all non-deterministic logic like database queries, external API calls, or random generators must live inside steps. Code placed outside steps re-runs on every single step execution.

Hard limits apply: each function allows at most 1,000 steps and 4MB of total step data. If you exceed those thresholds, execution breaks. The skill also enforces timeout handling on step.waitForEvent, reminding you that incoming events emitted before step execution cannot be caught retroactively. Code examples focus on TypeScript, while the core concepts apply across other supported runtimes.

What it helps with

  • pause workflow execution for hours or days without burning compute time
  • wait for incoming webhook callbacks or approvals with timeouts using step.waitForEvent
  • invoke secondary Inngest functions and consume their typed responses via step.invoke
  • isolate non-deterministic database and API operations inside step.run

Example prompts for the agent

  • Write an Inngest workflow that pauses for 24 hours using step.sleep before sending a reminder email
  • How do I wait for an invoice approval event with a 7-day timeout using step.waitForEvent?
  • Call an external function with step.invoke and return the processed result

How to install inngest-steps

1Skills CLI (Vercel)

npx skills add inngest/inngest-skills --skill inngest-steps -a claude-code

2Manually: put the files in place

~/.claude/skills/inngest-steps/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/inngest-steps/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about inngest-steps

How do I install inngest-steps in Claude Code?

Run npx skills add inngest/inngest-skills --skill inngest-steps -a claude-code. The skill file is placed in ~/.claude/skills/inngest-steps/SKILL.md.

What does the inngest-steps skill do?

It provides patterns and rules for building durable Inngest workflows in TypeScript. It details step methods such as step.run, step.sleep, step.waitForEvent, step.waitForSignal, and step.invoke.

What are the function limits in Inngest steps?

Each function supports a maximum of 1,000 steps and 4MB of total step data. Workflows exceeding these boundaries fail at the platform level.

Source text

SKILL.md · Apache-2.0
---
name: inngest-steps
description: "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 services without losing state on crashes, calling other functions and awaiting their results, memoizing expensive operations so they don't re-run on retry, or running async work in parallel inside a workflow. Covers Inngest step methods: step.run, step.sleep, step.waitForEvent, step.waitForSignal, step.sendEvent, step.invoke, step.ai, plus patterns for loops and parallel execution."
---

# 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](https://www.inngest.com/llms.txt) 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](../references/expressions.md) 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!

© inngest · Apache-2.0 · inngest/inngest-skills

Similar skills

All skills for Backend and APIs →

From the same set11

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor