← InngestCONTENT HISTORY
WHAT CHANGED
Update to Inngest
Snapshot Oct 24, 2026 · 13:41 UTC · version 0.4.1
Collection source: downloaded plugin package.
First saved snapshot
No earlier snapshot is available to establish a change.
Save Inngest and check future changes in Following.
Up to 10 plugins free · no card required.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"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.",
"included_files": [
{
"relative_path": "references/checkpointing.md",
"size_in_bytes": 14100
},
{
"relative_path": "references/error-handling.md",
"size_in_bytes": 12900
},
{
"relative_path": "references/observability.md",
"size_in_bytes": 14988
},
{
"relative_path": "references/step-execution.md",
"size_in_bytes": 10263
}
],
"name": "inngest-durable-functions",
"skill_md_contents": "---\nname: inngest-durable-functions\ndescription: 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.\n---\n\n# Inngest Durable Functions\n\nMaster Inngest's durable execution model for building fault-tolerant, long-running workflows. This skill covers the complete lifecycle from triggers to error handling.\n\n> **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.\n\n## Core Concepts You Need to Know\n\n### **Durable Execution Model**\n\n- **Each step** should encapsulate side-effects and non-deterministic code\n- **Memoization** prevents re-execution of completed steps\n- **State persistence** survives infrastructure failures\n- **Automatic retries** with configurable retry count\n\n### **Step Execution Flow**\n\n```typescript\n// ❌ BAD: Non-deterministic logic outside steps\nasync ({ event, step }) => {\n const timestamp = Date.now(); // This runs multiple times!\n\n const result = await step.run(\"process-data\", () => {\n return processData(event.data);\n });\n};\n\n// ✅ GOOD: All non-deterministic logic in steps\nasync ({ event, step }) => {\n const result = await step.run(\"process-with-timestamp\", () => {\n const timestamp = Date.now(); // Only runs once\n return processData(event.data, timestamp);\n });\n};\n```\n\n## Function Limits\n\n**Every Inngest function has these hard limits:**\n\n- **Maximum 1,000 steps** per function run\n- **Maximum 4MB** returned data for each step\n- **Maximum 32MB** combined function run state including, event data, step output, and function output\n- Each step = separate HTTP request (~50-100ms overhead)\n\nIf you're hitting these limits, break your function into smaller functions connected via `step.invoke()` or `step.sendEvent()`.\n\n## When to Use Steps\n\n**Always wrap in `step.run()`:**\n\n- API calls and network requests\n- Database reads and writes\n- File I/O operations\n- Any non-deterministic operation\n- Anything you want retried independently on failure\n\n**Never wrap in `step.run()`:**\n\n- Pure calculations and data transformations\n- Simple validation logic\n- Deterministic operations with no side effects\n- Logging (use outside steps)\n\n## Function Creation\n\n### Basic Function Structure\n\n```typescript\nconst processOrder = inngest.createFunction(\n {\n id: \"process-order\", // Unique, never change this\n triggers: [{ event: \"order/created\" }],\n retries: 4, // Default: 4 retries per step\n concurrency: 10 // Max concurrent executions\n },\n async ({ event, step }) => {\n // Your durable workflow\n }\n);\n```\n\n### **Step IDs and Memoization**\n\n```typescript\n// Step IDs can be reused - Inngest handles counters automatically\nconst data = await step.run(\"fetch-data\", () => fetchUserData());\nconst more = await step.run(\"fetch-data\", () => fetchOrderData()); // Different execution\n\n// Use descriptive IDs for clarity\nawait step.run(\"validate-payment\", () => validatePayment(event.data.paymentId));\nawait step.run(\"charge-customer\", () => chargeCustomer(event.data));\nawait step.run(\"send-confirmation\", () => sendEmail(event.data.email));\n```\n\n## Triggers and Events\n\n### **Event Triggers**\n\nTriggers are defined in the `triggers` array in the first argument of `createFunction`:\n\n```typescript\n// Single event trigger\ninngest.createFunction(\n { id: \"my-fn\", triggers: [{ event: \"user/signup\" }] },\n async ({ event }) => { /* ... */ }\n);\n\n// Event with conditional filter\ninngest.createFunction(\n { id: \"my-fn\", triggers: [{ event: \"user/action\", if: 'event.data.action == \"purchase\" && event.data.amount > 100' }] },\n async ({ event }) => { /* ... */ }\n);\n\n// Multiple triggers (up to 10)\ninngest.createFunction(\n {\n id: \"my-fn\",\n triggers: [\n { event: \"user/signup\" },\n { event: \"user/login\", if: 'event.data.firstLogin == true' },\n { cron: \"0 9 * * *\" } // Daily at 9 AM\n ]\n },\n async ({ event }) => { /* ... */ }\n);\n```\n\n### **Cron Triggers**\n\n```typescript\n// Basic cron\ninngest.createFunction(\n { id: \"my-fn\", triggers: [{ cron: \"0 */6 * * *\" }] }, // Every 6 hours\n async ({ step }) => { /* ... */ }\n);\n\n// With timezone\ninngest.createFunction(\n { id: \"my-fn\", triggers: [{ cron: \"TZ=Europe/Paris 0 12 * * 5\" }] }, // Fridays at noon Paris time\n async ({ step }) => { /* ... */ }\n);\n\n// Combine with events\ninngest.createFunction(\n {\n id: \"my-fn\",\n triggers: [\n { event: \"manual/report.requested\" },\n { cron: \"0 0 * * 0\" } // Weekly on Sunday\n ]\n },\n async ({ event, step }) => { /* ... */ }\n);\n```\n\n### **Function Invocation**\n\n```typescript\n// Invoke another function as a step\nconst result = await step.invoke(\"generate-report\", {\n function: generateReportFunction,\n data: { userId: event.data.userId }\n});\n\n// Use returned data\nawait step.run(\"process-report\", () => {\n return processReport(result);\n});\n```\n\n## Idempotency Strategies\n\n### **Event-Level Idempotency (Producer Side)**\n\n```typescript\n// Prevent duplicate events with custom ID\nawait inngest.send({\n id: `checkout-completed-${cartId}`, // 24-hour deduplication\n name: \"cart/checkout.completed\",\n data: { cartId, email: \"user@example.com\" }\n});\n```\n\n### **Function-Level Idempotency (Consumer Side)**\n\n```typescript\nconst sendEmail = inngest.createFunction(\n {\n id: \"send-checkout-email\",\n triggers: [{ event: \"cart/checkout.completed\" }],\n // Only run once per cartId per 24 hours\n idempotency: \"event.data.cartId\"\n },\n async ({ event, step }) => {\n // This function won't run twice for same cartId\n }\n);\n\n// Complex idempotency keys\nconst processUserAction = inngest.createFunction(\n {\n id: \"process-user-action\",\n triggers: [{ event: \"user/action.performed\" }],\n // Unique per user + organization combination\n idempotency: 'event.data.userId + \"-\" + event.data.organizationId'\n },\n async ({ event, step }) => {\n /* ... */\n }\n);\n```\n\n## Cancellation Patterns\n\n### **Event-Based Cancellation**\n\nIn expressions, `event` = the **original** triggering event, `async` = the **new** event being matched. See [Expression Syntax Reference](../references/expressions.md) for full details.\n\n```typescript\nconst processOrder = inngest.createFunction(\n {\n id: \"process-order\",\n triggers: [{ event: \"order/created\" }],\n cancelOn: [\n {\n event: \"order/cancelled\",\n if: \"event.data.orderId == async.data.orderId\"\n }\n ]\n },\n async ({ event, step }) => {\n await step.sleepUntil(\"wait-for-payment\", event.data.paymentDue);\n // Will be cancelled if order/cancelled event received\n await step.run(\"charge-payment\", () => processPayment(event.data));\n }\n);\n```\n\n### **Timeout Cancellation**\n\n```typescript\nconst processWithTimeout = inngest.createFunction(\n {\n id: \"process-with-timeout\",\n triggers: [{ event: \"long/process.requested\" }],\n timeouts: {\n start: \"5m\", // Cancel if not started within 5 minutes\n finish: \"30m\" // Cancel if not finished within 30 minutes\n }\n },\n async ({ event, step }) => {\n /* ... */\n }\n);\n```\n\n### **Handling Cancellation Cleanup**\n\n```typescript\n// Listen for cancellation events\nconst cleanupCancelled = inngest.createFunction(\n { id: \"cleanup-cancelled-process\", triggers: [{ event: \"inngest/function.cancelled\" }] },\n async ({ event, step }) => {\n if (event.data.function_id === \"process-order\") {\n await step.run(\"cleanup-resources\", () => {\n return cleanupOrderResources(event.data.run_id);\n });\n }\n }\n);\n```\n\n## Error Handling and Retries\n\n### **Default Retry Behavior**\n\n- **5 total attempts** (1 initial + 4 retries) per step\n- **Exponential backoff** with jitter\n- **Independent retry counters** per step\n\n### **Custom Retry Configuration**\n\n```typescript\nconst reliableFunction = inngest.createFunction(\n {\n id: \"reliable-function\",\n triggers: [{ event: \"critical/task\" }],\n retries: 10 // Up to 10 retries per step\n },\n async ({ event, step, attempt }) => {\n // `attempt` is the function-level attempt counter (0-indexed)\n // It tracks retries for the currently executing step, not the overall function\n if (attempt > 5) {\n // Different logic for later attempts of the current step\n }\n }\n);\n```\n\n### **Non-Retriable Errors**\n\nPrevent retries for code that won't succeed upon retry.\n\n```typescript\nimport { NonRetriableError } from \"inngest\";\n\nconst processUser = inngest.createFunction(\n { id: \"process-user\", triggers: [{ event: \"user/process.requested\" }] },\n async ({ event, step }) => {\n const user = await step.run(\"fetch-user\", async () => {\n const user = await db.users.findOne(event.data.userId);\n\n if (!user) {\n // Don't retry - user doesn't exist\n throw new NonRetriableError(\"User not found, stopping execution\");\n }\n\n return user;\n });\n\n // Continue processing...\n }\n);\n```\n\n### **Custom Retry Timing**\n\n```typescript\nimport { RetryAfterError } from \"inngest\";\n\nconst respectRateLimit = inngest.createFunction(\n { id: \"api-call\", triggers: [{ event: \"api/call.requested\" }] },\n async ({ event, step }) => {\n await step.run(\"call-api\", async () => {\n const response = await externalAPI.call(event.data);\n\n if (response.status === 429) {\n // Retry after specific time from API\n const retryAfter = response.headers[\"retry-after\"];\n throw new RetryAfterError(\"Rate limited\", `${retryAfter}s`);\n }\n\n return response.data;\n });\n }\n);\n```\n\n## Logging Best Practices\n\n### **Proper Logging Setup**\n\n```typescript\nimport winston from \"winston\";\n\n// Configure logger\nconst logger = winston.createLogger({\n level: \"info\",\n format: winston.format.json(),\n transports: [new winston.transports.Console()]\n});\n\nconst inngest = new Inngest({\n id: \"my-app\",\n logger // Pass logger to client\n});\n\n// Or use the built-in ConsoleLogger for simple log level control\nimport { ConsoleLogger, Inngest } from \"inngest\";\n\nconst inngest = new Inngest({\n id: \"my-app\",\n logger: new ConsoleLogger({ level: \"debug\" }) // \"debug\" | \"info\" | \"warn\" | \"error\"\n});\n```\n\n**⚠️ v4 Breaking Change:** The `logLevel` option has been removed. Use the `logger` option with `ConsoleLogger` or a custom logger instead.\n\n### **Function Logging Patterns**\n\n```typescript\nconst processData = inngest.createFunction(\n { id: \"process-data\", triggers: [{ event: \"data/process.requested\" }] },\n async ({ event, step, logger }) => {\n // ✅ GOOD: Log inside steps to avoid duplicates\n const result = await step.run(\"fetch-data\", async () => {\n logger.info(\"Fetching data for user\", { userId: event.data.userId });\n return await fetchUserData(event.data.userId);\n });\n\n // ❌ AVOID: Logging outside steps can duplicate\n // logger.info(\"Processing complete\"); // This could run multiple times!\n\n await step.run(\"log-completion\", async () => {\n logger.info(\"Processing complete\", { resultCount: result.length });\n });\n }\n);\n```\n\n## Performance Optimization\n\n### **Checkpointing**\n\nCheckpointing is **enabled by default in v4**. It allows functions to persist state periodically during execution, reducing latency between steps.\n\n```typescript\n// Checkpointing is enabled by default in v4\n// Configure maxRuntime for serverless platforms (set to 60-80% of platform timeout)\nconst realTimeFunction = inngest.createFunction(\n {\n id: \"real-time-function\",\n triggers: [{ event: \"realtime/process\" }],\n checkpointing: {\n maxRuntime: \"50s\", // For serverless with 60s timeout\n }\n },\n async ({ event, step }) => {\n // Steps execute immediately with periodic checkpointing\n const result1 = await step.run(\"step-1\", () => process1(event.data));\n const result2 = await step.run(\"step-2\", () => process2(result1));\n return { result2 };\n }\n);\n\n// Disable checkpointing if needed\nconst legacyFunction = inngest.createFunction(\n {\n id: \"legacy-function\",\n triggers: [{ event: \"legacy/process\" }],\n checkpointing: false\n },\n async ({ event, step }) => { /* ... */ }\n);\n```\n\n## Advanced Patterns\n\n### **Conditional Step Execution**\n\n```typescript\nconst conditionalProcess = inngest.createFunction(\n { id: \"conditional-process\", triggers: [{ event: \"process/conditional\" }] },\n async ({ event, step }) => {\n const userData = await step.run(\"fetch-user\", () => {\n return getUserData(event.data.userId);\n });\n\n // Conditional step execution\n if (userData.isPremium) {\n await step.run(\"premium-processing\", () => {\n return processPremiumFeatures(userData);\n });\n }\n\n // Always runs\n await step.run(\"standard-processing\", () => {\n return processStandardFeatures(userData);\n });\n }\n);\n```\n\n### **Error Recovery Patterns**\n\n```typescript\nconst robustProcess = inngest.createFunction(\n { id: \"robust-process\", triggers: [{ event: \"process/robust\" }] },\n async ({ event, step }) => {\n let primaryResult;\n\n try {\n primaryResult = await step.run(\"primary-service\", () => {\n return callPrimaryService(event.data);\n });\n } catch (error) {\n // Fallback to secondary service\n primaryResult = await step.run(\"fallback-service\", () => {\n return callSecondaryService(event.data);\n });\n }\n\n return { result: primaryResult };\n }\n);\n```\n\n## Common Mistakes to Avoid\n\n1. **❌ Non-deterministic code outside steps**\n2. **❌ Database calls outside steps**\n3. **❌ Logging outside steps (causes duplicates)**\n4. **❌ Changing step IDs after deployment**\n5. **❌ Not handling NonRetriableError cases**\n6. **❌ Ignoring idempotency for critical functions**\n\n## Next Steps\n\n- See **inngest-steps** for detailed step method reference\n- See [references/step-execution.md](references/step-execution.md) for detailed step patterns\n- See [references/error-handling.md](references/error-handling.md) for comprehensive error strategies\n- See [references/observability.md](references/observability.md) for monitoring and tracing setup\n- See [references/checkpointing.md](references/checkpointing.md) for performance optimization details\n\n---\n\n_This skill covers Inngest's durable function patterns. For event sending and webhook handling, see the `inngest-events` skill._\n"
}SHA-256 of public snapshot: 43aa63d4b729736a39a29f93ac0869e346d5c99ca5717649e3ce3e118e613ec6