{"id":50031,"plugin_id":"plugin_asdk_app_6abe99ad4764819183b795f0d74e5216","kind":"skill","collection_source":"plugin_package","comparison_source":null,"observed_at":"2026-10-24T13:41:54.644Z","digest":"43aa63d4b729736a39a29f93ac0869e346d5c99ca5717649e3ce3e118e613ec6","against":null,"payload":{"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"},"changes":[],"summary":"First saved snapshot. No earlier version is available for comparison.","summary_kind":"deterministic","summary_metadata":{}}