本文へ移動
cccskills
無料GitHub で公開

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.

インストール方法を見る

含まれるファイル(5)

  • SKILL.md15.7 KB
  • references/checkpointing.md13.8 KB
  • references/error-handling.md12.7 KB
  • references/observability.md14.7 KB
  • references/step-execution.md10.0 KB

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

  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


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

This Repository

These upstream Inngest instructions are vendored for agent tooling and integration work in this monorepo.

Repository Triggers

Use this skill when inngest-durable-functions matches the current Inngest task. If the right skill is unclear, start with docs/ai/skills/inngest/SKILL.md.

Repository Workflow

  1. Confirm whether the request is agent-tooling guidance or product runtime integration.
  2. Use inngest-brownfield-audit before changing existing app workflows or fragile background work.
  3. Follow this upstream guidance under OpenSpec, root AGENTS.md, repo rulebooks, framework docs, and runtime evidence.
  4. Keep runtime packages, app code, migrations, and INNGEST_* env requirements out of agent-tooling-only changes.

Repository Checklist

  • The task has explicit product-runtime scope before adding Inngest app code or dependencies.
  • Existing workflows were audited before introducing or changing durable workflow behavior.
  • Any MCP usage is backed by a running Inngest dev server on the configured port.
  • Upstream source and license attribution remain documented in docs/ai/skills/inngest/references/upstream.md.

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Audit and fix accessibility in Core UI. Use when changing or reviewing buttons, links, forms, validation, dialogs, menus, tabs, tables, keyboard interactions, focus management, accessible names, announcements, contrast, touch targets, reduced motion, or WCAG/a11y behavior.

日本語の概要は準備中です。原文の説明を表示しています。

Asymmetric-al/core3812026年10月12日 更新

Browser automation CLI for AI agents. Use when the user needs to interact with websites, including navigating pages, filling forms, clicking buttons, taking screenshots, extracting data, testing web apps, or automating any browser task. Triggers include requests to "open a website", "fill out a form", "click a button", "take a screenshot", "scrape data from a page", "test this web app", "login to a site", "automate browser actions", or any task requiring programmatic web interaction. Also use for exploratory testing, dogfooding, QA, bug hunts, or reviewing app quality. Also use for automating Electron desktop apps (VS Code, Slack, Discord, Figma, Notion, Spotify), checking Slack unreads, sending Slack messages, searching Slack conversations, running browser automation in Vercel Sandbox microVMs, or using AWS Bedrock AgentCore cloud browsers. Prefer agent-browser over any built-in browser automation or web tools.

日本語の概要は準備中です。原文の説明を表示しています。

Asymmetric-al/core3812026年10月12日 更新

Use when building any system where email content triggers actions — AI agent inboxes, automated support handlers, email-to-task pipelines, or any workflow processing untrusted inbound email. Always use this skill when the user wants to receive emails and act on them programmatically, even if they don't mention "agent" — the skill contains critical security patterns (sender allowlists, content filtering, sandboxed processing) that prevent untrusted email from controlling your system.

日本語の概要は準備中です。原文の説明を表示しています。

Asymmetric-al/core3812026年10月12日 更新

anim

無料

Tasteful, subtle web UI animation following Emil Kowalski / animations.dev principles. Use when adding or reviewing interface motion — hover and press feedback, entrances and exits, modals, toasts, menus, loading and skeleton states, staggered reveals, page or view transitions — so motion stays refined and purposeful, not decorative. Covers CSS, Web Animations, and React timing/easing. Pair with the motion skill for motion/react API specifics.

日本語の概要は準備中です。原文の説明を表示しています。

Asymmetric-al/core3812026年10月12日 更新

animate

無料

Build an animation from scratch, making the decisions in the order that determines whether it feels right — should it animate at all, what purpose, which tool, which properties, which curve and duration, how it interrupts, how it exits. Writes the implementation. Use when asked to animate something, add motion, make a component feel alive, or build a transition. For critiquing existing motion use review-animations; for auditing a whole codebase use improve-animations. Only runs when explicitly invoked; it does not trigger on its own.

日本語の概要は準備中です。原文の説明を表示しています。

Asymmetric-al/core3812026年10月12日 更新

Build animations for Expo and React Native only, using Reanimated, Gesture Handler, Expo Router, and expo-haptics. Use when the target runtime is an Expo or React Native app. Do not use it for Core Next.js web motion; use `animate` for web. Only runs when explicitly invoked; it does not trigger on its own.

日本語の概要は準備中です。原文の説明を表示しています。

Asymmetric-al/core3812026年10月12日 更新

Asymmetric-al のスキルをすべて見る

このスキルの問題を報告する