Skip to content
Inngest Durable Functions

Inngest Durable FunctionsSkill

Released
Apache-2.0
Repository

Summary

Inngest's durable-execution playbook as an agent skill — steps and memoisation, event and cron triggers, idempotency, cancellation, retries and non-retriable errors.

Features

  • Puts non-deterministic code and side effects inside steps — the rule agents most often break
  • Step IDs and memoisation explained so replays stay correct
  • Event, cron and invoke triggers with working configuration for each
  • Event-level and function-level idempotency, and when to use which
  • Event-based and timeout cancellation, including cleanup handling
  • Retry configuration, NonRetriableError and custom retry timing
  • Replay-safe structured logging and checkpointing for performance

Install This Skill

Add this skill to your favorite AI agent in a few steps.

Any AI agent

This skill is plain instructions — it works with any assistant that accepts custom instructions or system prompts.

  1. Copy the skill content with the button below.
  2. Paste it into your agent's instruction file or system prompt (for example AGENTS.md, .cursorrules, or a custom instructions field).
  3. Ask the agent to apply the skill whenever the task matches.

Skill Content

Markdown Content

Copy this content and use it with your preferred AI agent

---
name: inngest-durable-functions
description: 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.
---

# Inngest Durable Functions

Master Inngest's durable execution model for building fault-tolerant, long-running workflows. This skill covers the complete lifecycle from triggers to error handling.

> **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 Concepts You Need to Know

### **Durable Execution Model**

- **Each step** should encapsulate side-effects and non-deterministic code
- **Memoization** prevents re-execution of completed steps
- **State persistence** survives infrastructure failures
- **Automatic retries** with configurable retry count

### **Step Execution Flow**

```typescript
// ❌ BAD: Non-deterministic logic outside steps
async ({ event, step }) => {
  const timestamp = Date.now(); // This runs multiple times!

  const result = await step.run("process-data", () => {
    return processData(event.data);
  });
};

// ✅ GOOD: All non-deterministic logic in steps
async ({ event, step }) => {
  const result = await step.run("process-with-timestamp", () => {
    const timestamp = Date.now(); // Only runs once
    return processData(event.data, timestamp);
  });
};
```

## Function Limits

**Every Inngest function has these hard limits:**

- **Maximum 1,000 steps** per function run
- **Maximum 4MB** returned data for each step
- **Maximum 32MB** combined function run state including, event data, step output, and function output
- Each step = separate HTTP request (~50-100ms overhead)

If you're hitting these limits, break your function into smaller functions connected via `step.invoke()` or `step.sendEvent()`.

## When to Use Steps

**Always wrap in `step.run()`:**

- API calls and network requests
- Database reads and writes
- File I/O operations
- Any non-deterministic operation
- Anything you want retried independently on failure

**Never wrap in `step.run()`:**

- Pure calculations and data transformations
- Simple validation logic
- Deterministic operations with no side effects
- Logging (use outside steps)

## Function Creation

### Basic Function Structure

```typescript
const processOrder = inngest.createFunction(
  {
    id: "process-order", // Unique, never change this
    triggers: [{ event: "order/created" }],
    retries: 4, // Default: 4 retries per step
    concurrency: 10 // Max concurrent executions
  },
  async ({ event, step }) => {
    // Your durable workflow
  }
);
```

### **Step IDs and Memoization**

```typescript
// Step IDs can be reused - Inngest handles counters automatically
const data = await step.run("fetch-data", () => fetchUserData());
const more = await step.run("fetch-data", () => fetchOrderData()); // Different execution

// Use descriptive IDs for clarity
await step.run("validate-payment", () => validatePayment(event.data.paymentId));
await step.run("charge-customer", () => chargeCustomer(event.data));
await step.run("send-confirmation", () => sendEmail(event.data.email));
```

## Triggers and Events

### **Event Triggers**

Triggers are defined in the `triggers` array in the first argument of `createFunction`:

```typescript
// Single event trigger
inngest.createFunction(
  { id: "my-fn", triggers: [{ event: "user/signup" }] },
  async ({ event }) => { /* ... */ }
);

// Event with conditional filter
inngest.createFunction(
  { id: "my-fn", triggers: [{ event: "user/action", if: 'event.data.action == "purchase" && event.data.amount > 100' }] },
  async ({ event }) => { /* ... */ }
);

// Multiple triggers (up to 10)
inngest.createFunction(
  {
    id: "my-fn",
    triggers: [
      { event: "user/signup" },
      { event: "user/login", if: 'event.data.firstLogin == true' },
      { cron: "0 9 * * *" } // Daily at 9 AM
    ]
  },
  async ({ event }) => { /* ... */ }
);
```

### **Cron Triggers**

```typescript
// Basic cron
inngest.createFunction(
  { id: "my-fn", triggers: [{ cron: "0 */6 * * *" }] }, // Every 6 hours
  async ({ step }) => { /* ... */ }
);

// With timezone
inngest.createFunction(
  { id: "my-fn", triggers: [{ cron: "TZ=Europe/Paris 0 12 * * 5" }] }, // Fridays at noon Paris time
  async ({ step }) => { /* ... */ }
);

// Combine with events
inngest.createFunction(
  {
    id: "my-fn",
    triggers: [
      { event: "manual/report.requested" },
      { cron: "0 0 * * 0" } // Weekly on Sunday
    ]
  },
  async ({ event, step }) => { /* ... */ }
);
```

### **Function Invocation**

```typescript
// Invoke another function as a step
const result = await step.invoke("generate-report", {
  function: generateReportFunction,
  data: { userId: event.data.userId }
});

// Use returned data
await step.run("process-report", () => {
  return processReport(result);
});
```

## Idempotency Strategies

### **Event-Level Idempotency (Producer Side)**

```typescript
// Prevent duplicate events with custom ID
await inngest.send({
  id: `checkout-completed-${cartId}`, // 24-hour deduplication
  name: "cart/checkout.completed",
  data: { cartId, email: "user@example.com" }
});
```

### **Function-Level Idempotency (Consumer Side)**

```typescript
const sendEmail = inngest.createFunction(
  {
    id: "send-checkout-email",
    triggers: [{ event: "cart/checkout.completed" }],
    // Only run once per cartId per 24 hours
    idempotency: "event.data.cartId"
  },
  async ({ event, step }) => {
    // This function won't run twice for same cartId
  }
);

// Complex idempotency keys
const processUserAction = inngest.createFunction(
  {
    id: "process-user-action",
    triggers: [{ event: "user/action.performed" }],
    // Unique per user + organization combination
    idempotency: 'event.data.userId + "-" + event.data.organizationId'
  },
  async ({ event, step }) => {
    /* ... */
  }
);
```

## Cancellation Patterns

### **Event-Based Cancellation**

In expressions, `event` = the **original** triggering event, `async` = the **new** event being matched. See [Expression Syntax Reference](../references/expressions.md) for full details.

```typescript
const processOrder = inngest.createFunction(
  {
    id: "process-order",
    triggers: [{ event: "order/created" }],
    cancelOn: [
      {
        event: "order/cancelled",
        if: "event.data.orderId == async.data.orderId"
      }
    ]
  },
  async ({ event, step }) => {
    await step.sleepUntil("wait-for-payment", event.data.paymentDue);
    // Will be cancelled if order/cancelled event received
    await step.run("charge-payment", () => processPayment(event.data));
  }
);
```

### **Timeout Cancellation**

```typescript
const processWithTimeout = inngest.createFunction(
  {
    id: "process-with-timeout",
    triggers: [{ event: "long/process.requested" }],
    timeouts: {
      start: "5m", // Cancel if not started within 5 minutes
      finish: "30m" // Cancel if not finished within 30 minutes
    }
  },
  async ({ event, step }) => {
    /* ... */
  }
);
```

### **Handling Cancellation Cleanup**

```typescript
// Listen for cancellation events
const cleanupCancelled = inngest.createFunction(
  { id: "cleanup-cancelled-process", triggers: [{ event: "inngest/function.cancelled" }] },
  async ({ event, step }) => {
    if (event.data.function_id === "process-order") {
      await step.run("cleanup-resources", () => {
        return cleanupOrderResources(event.data.run_id);
      });
    }
  }
);
```

## Error Handling and Retries

### **Default Retry Behavior**

- **5 total attempts** (1 initial + 4 retries) per step
- **Exponential backoff** with jitter
- **Independent retry counters** per step

### **Custom Retry Configuration**

```typescript
const reliableFunction = inngest.createFunction(
  {
    id: "reliable-function",
    triggers: [{ event: "critical/task" }],
    retries: 10 // Up to 10 retries per step
  },
  async ({ event, step, attempt }) => {
    // `attempt` is the function-level attempt counter (0-indexed)
    // It tracks retries for the currently executing step, not the overall function
    if (attempt > 5) {
      // Different logic for later attempts of the current step
    }
  }
);
```

### **Non-Retriable Errors**

Prevent retries for code that won't succeed upon retry.

```typescript
import { NonRetriableError } from "inngest";

const processUser = inngest.createFunction(
  { id: "process-user", triggers: [{ event: "user/process.requested" }] },
  async ({ event, step }) => {
    const user = await step.run("fetch-user", async () => {
      const user = await db.users.findOne(event.data.userId);

      if (!user) {
        // Don't retry - user doesn't exist
        throw new NonRetriableError("User not found, stopping execution");
      }

      return user;
    });

    // Continue processing...
  }
);
```

### **Custom Retry Timing**

```typescript
import { RetryAfterError } from "inngest";

const respectRateLimit = inngest.createFunction(
  { id: "api-call", triggers: [{ event: "api/call.requested" }] },
  async ({ event, step }) => {
    await step.run("call-api", async () => {
      const response = await externalAPI.call(event.data);

      if (response.status === 429) {
        // Retry after specific time from API
        const retryAfter = response.headers["retry-after"];
        throw new RetryAfterError("Rate limited", `${retryAfter}s`);
      }

      return response.data;
    });
  }
);
```

## Logging Best Practices

### **Proper Logging Setup**

```typescript
import winston from "winston";

// Configure logger
const logger = winston.createLogger({
  level: "info",
  format: winston.format.json(),
  transports: [new winston.transports.Console()]
});

const inngest = new Inngest({
  id: "my-app",
  logger // Pass logger to client
});

// Or use the built-in ConsoleLogger for simple log level control
import { ConsoleLogger, Inngest } from "inngest";

const inngest = new Inngest({
  id: "my-app",
  logger: new ConsoleLogger({ level: "debug" }) // "debug" | "info" | "warn" | "error"
});
```

**⚠️ v4 Breaking Change:** The `logLevel` option has been removed. Use the `logger` option with `ConsoleLogger` or a custom logger instead.

### **Function Logging Patterns**

```typescript
const processData = inngest.createFunction(
  { id: "process-data", triggers: [{ event: "data/process.requested" }] },
  async ({ event, step, logger }) => {
    // ✅ GOOD: Log inside steps to avoid duplicates
    const result = await step.run("fetch-data", async () => {
      logger.info("Fetching data for user", { userId: event.data.userId });
      return await fetchUserData(event.data.userId);
    });

    // ❌ AVOID: Logging outside steps can duplicate
    // logger.info("Processing complete"); // This could run multiple times!

    await step.run("log-completion", async () => {
      logger.info("Processing complete", { resultCount: result.length });
    });
  }
);
```

## Performance Optimization

### **Checkpointing**

Checkpointing is **enabled by default in v4**. It allows functions to persist state periodically during execution, reducing latency between steps.

```typescript
// Checkpointing is enabled by default in v4
// Configure maxRuntime for serverless platforms (set to 60-80% of platform timeout)
const realTimeFunction = inngest.createFunction(
  {
    id: "real-time-function",
    triggers: [{ event: "realtime/process" }],
    checkpointing: {
      maxRuntime: "50s", // For serverless with 60s timeout
    }
  },
  async ({ event, step }) => {
    // Steps execute immediately with periodic checkpointing
    const result1 = await step.run("step-1", () => process1(event.data));
    const result2 = await step.run("step-2", () => process2(result1));
    return { result2 };
  }
);

// Disable checkpointing if needed
const legacyFunction = inngest.createFunction(
  {
    id: "legacy-function",
    triggers: [{ event: "legacy/process" }],
    checkpointing: false
  },
  async ({ event, step }) => { /* ... */ }
);
```

## Advanced Patterns

### **Conditional Step Execution**

```typescript
const conditionalProcess = inngest.createFunction(
  { id: "conditional-process", triggers: [{ event: "process/conditional" }] },
  async ({ event, step }) => {
    const userData = await step.run("fetch-user", () => {
      return getUserData(event.data.userId);
    });

    // Conditional step execution
    if (userData.isPremium) {
      await step.run("premium-processing", () => {
        return processPremiumFeatures(userData);
      });
    }

    // Always runs
    await step.run("standard-processing", () => {
      return processStandardFeatures(userData);
    });
  }
);
```

### **Error Recovery Patterns**

```typescript
const robustProcess = inngest.createFunction(
  { id: "robust-process", triggers: [{ event: "process/robust" }] },
  async ({ event, step }) => {
    let primaryResult;

    try {
      primaryResult = await step.run("primary-service", () => {
        return callPrimaryService(event.data);
      });
    } catch (error) {
      // Fallback to secondary service
      primaryResult = await step.run("fallback-service", () => {
        return callSecondaryService(event.data);
      });
    }

    return { result: primaryResult };
  }
);
```

## Common Mistakes to Avoid

1. **❌ Non-deterministic code outside steps**
2. **❌ Database calls outside steps**
3. **❌ Logging outside steps (causes duplicates)**
4. **❌ Changing step IDs after deployment**
5. **❌ Not handling NonRetriableError cases**
6. **❌ Ignoring idempotency for critical functions**

## Next Steps

- See **inngest-steps** for detailed step method reference
- See [references/step-execution.md](references/step-execution.md) for detailed step patterns
- See [references/error-handling.md](references/error-handling.md) for comprehensive error strategies
- See [references/observability.md](references/observability.md) for monitoring and tracing setup
- See [references/checkpointing.md](references/checkpointing.md) for performance optimization details

---

_This skill covers Inngest's durable function patterns. For event sending and webhook handling, see the `inngest-events` skill._

Usage Instructions

Learn how to use this skill with different AI agents.

Claude Desktop
/plugin marketplace add inngest/inngest-claude-code-plugin
/plugin install inngest@inngest-claude-code-plugin

Installs all fourteen Inngest skills at once. They then trigger automatically when you work on Inngest code. The same plugin commands work in Codex.

Example Usage

Turn this webhook handler into an Inngest durable function so retries don't double-charge the customer.

Description

Durable execution is easy to get subtly wrong, and the failure mode is quiet: a workflow that looks correct replays a Date.now() on every retry, or writes the same Stripe charge twice, and nobody notices until production. This skill from the Inngest team gives a coding agent the model it needs to avoid that class of bug while writing the code.

It leads with the rule that matters most. Every non-deterministic operation and every side effect must live inside a step.run, because completed steps are memoised and everything outside them re-executes on each replay. The skill opens with a bad/good pair showing exactly that — a timestamp computed in the function body versus inside a step — before it gets anywhere near API surface. That ordering is deliberate, and it is the correction most agents need.

What it covers. Function configuration and limits; the three trigger types (event, cron, invoke) with concrete configuration for each; step IDs and how memoisation keys off them; idempotency at both ends — an event-level key on the producer side and a function-level key on the consumer side, with guidance on when each is the right one; cancellation, both event-based (cancelOn) and timeout-based, plus cleanup handling; error handling with default and custom retry counts, NonRetriableError for failures that will never succeed, and custom retry timing; structured logging that survives replay; and checkpointing for performance.

Scope. The examples are TypeScript. Inngest's Python and Go SDKs share the concepts but not the syntax, and the skill says so up front and points at the docs rather than pretending otherwise.

Part of the fourteen-skill inngest/inngest-skills set, Apache-2.0 licensed.

Related Skills

New

Build crash-safe AI agents on AgentKit and step.ai — durable tool calls, human-in-the-loop approval, realtime progress, and when not to reach for an agent loop at all.

New

Temporal's official skill for building durable workflows — SDK patterns across seven languages, plus the determinism rules that decide whether a workflow survives a replay.

5 views
New

Expo's official skill for building native-feeling screens: Apple HIG styling, semantic colors, SF Symbols, native controls, Reanimated, blur and liquid glass.

3 views
New

Pull unresolved CodeRabbit review threads from your PR and apply the fixes one at a time, treating every reviewer comment as untrusted input rather than an instruction.

5 views
Browse all skills →