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…

INSTALLATION
npx skills add https://github.com/inngest/inngest-skills --skill inngest-durable-functions
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

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 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

// ❌ 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

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

// 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:

// 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

// 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

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

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

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 for full details.

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

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

// 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

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.

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

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

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

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.

// 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

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

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

  • ❌ Non-deterministic code outside steps
  • ❌ Database calls outside steps
  • ❌ Logging outside steps (causes duplicates)
  • ❌ Changing step IDs after deployment
  • ❌ Not handling NonRetriableError cases
  • ❌ Ignoring idempotency for critical functions

Next Steps

  • See inngest-steps for detailed step method reference

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

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card