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.
What this skill does
# 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: "[email protected]" }
});
```
### **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, noRelated in Web Dev
generating-lwc-components
IncludedLightning Web Components with PICKLES methodology and 165-point scoring. Use this skill when the user creates or edits LWC components, builds wire service patterns, or writes Jest tests for LWC. TRIGGER when: user creates/edits LWC components, touches lwc/**/*.js, .html, .css, .js-meta.xml files, or asks about wire service, SLDS, or Jest LWC tests. DO NOT TRIGGER when: Apex classes (use generating-apex), Aura components, or Visualforce.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Set up queries with useQuery, mutations with useMutation, configure QueryClient caching strategies, implement optimistic updates, and handle infinite scroll with useInfiniteQuery. Use when: setting up data fetching in React projects, migrating from v4 to v5, or fixing object syntax required errors, query callbacks removed issues, cacheTime renamed to gcTime, isPending vs isLoading confusion, keepPreviousData removed problems.
document-processor-api
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
nutrient-document-processing
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Covers useMutationState, simplified optimistic updates, throwOnError, network mode (offline/PWA), and infiniteQueryOptions. Use when setting up data fetching, fixing v4→v5 migration errors (object syntax, gcTime, isPending, keepPreviousData), or debugging SSR/hydration issues with streaming server components.
accelint-nextjs-best-practices
IncludedNext.js performance optimization and best practices. Use when writing Next.js code (App Router or Pages Router); implementing Server Components, Server Actions, or API routes; optimizing RSC serialization, data fetching, or server-side rendering; reviewing Next.js code for performance issues; fixing authentication in Server Actions; or implementing Suspense boundaries, parallel data fetching, or request deduplication.