← InngestCONTENT HISTORY

Update to Inngest

Snapshot Oct 24, 2026 · 13:41 UTC · version 0.4.1

Collection source: downloaded plugin package.

WHAT CHANGED

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 JSON
Full technical diff · 0 changed fields
Full snapshot data
{
  "description": "Use when designing event-driven workflows, decoupling services, implementing fan-out patterns (one trigger, many downstream handlers), implementing idempotent event handling with IDs (24-hour dedupe window), or handling at-least-once delivery from external sources like Stripe webhooks. Covers Inngest event schema, payload format, naming conventions, IDs for idempotency, the ts param, fan-out patterns, and system events like inngest/function.failed.",
  "included_files": [],
  "name": "inngest-events",
  "skill_md_contents": "---\nname: inngest-events\ndescription: Use when designing event-driven workflows, decoupling services, implementing fan-out patterns (one trigger, many downstream handlers), implementing idempotent event handling with IDs (24-hour dedupe window), or handling at-least-once delivery from external sources like Stripe webhooks. Covers Inngest event schema, payload format, naming conventions, IDs for idempotency, the ts param, fan-out patterns, and system events like inngest/function.failed.\n---\n\n# Inngest Events\n\nMaster Inngest event design and delivery patterns. Events are the foundation of Inngest - learn to design robust event schemas, implement idempotency, leverage fan-out patterns, and handle system events effectively.\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## Event Payload Format\n\nEvery Inngest event is a JSON object with required and optional properties:\n\n### Required Properties\n\n```typescript\ntype Event = {\n  name: string; // Event type (triggers functions)\n  data: object; // Payload data (any nested JSON)\n};\n```\n\n### Complete Schema\n\n```typescript\ntype EventPayload = {\n  name: string; // Required: event type\n  data: Record<string, any>; // Required: event data\n  id?: string; // Optional: deduplication ID\n  ts?: number; // Optional: timestamp (Unix ms)\n  v?: string; // Optional: schema version\n};\n```\n\n### Basic Event Example\n\n```typescript\nawait inngest.send({\n  name: \"billing/invoice.paid\",\n  data: {\n    customerId: \"cus_NffrFeUfNV2Hib\",\n    invoiceId: \"in_1J5g2n2eZvKYlo2C0Z1Z2Z3Z\",\n    userId: \"user_03028hf09j2d02\",\n    amount: 1000,\n    metadata: {\n      accountId: \"acct_1J5g2n2eZvKYlo2C0Z1Z2Z3Z\",\n      accountName: \"Acme.ai\"\n    }\n  }\n});\n```\n\n## Event Naming Conventions\n\n**Use the Object-Action pattern:** `domain/noun.verb`\n\n### Recommended Patterns\n\n```typescript\n// ✅ Good: Clear object-action pattern\n\"billing/invoice.paid\";\n\"user/profile.updated\";\n\"order/item.shipped\";\n\"ai/summary.completed\";\n\n// ✅ Good: Domain prefixes for organization\n\"stripe/customer.created\";\n\"intercom/conversation.assigned\";\n\"slack/message.posted\";\n\n// ❌ Avoid: Unclear or inconsistent\n\"payment\"; // What happened?\n\"user_update\"; // Use dots, not underscores\n\"invoiceWasPaid\"; // Too verbose\n```\n\n### Naming Guidelines\n\n- **Past tense:** Events describe what happened (`created`, `updated`, `failed`)\n- **Dot notation:** Use dots for hierarchy (`billing/invoice.paid`)\n- **Prefixes:** Group related events (`api/user.created`, `webhook/stripe.received`)\n- **Consistency:** Establish patterns and stick to them\n\n## Event IDs and Idempotency\n\n**When to use IDs:** Prevent duplicate processing when events might be sent multiple times.\n\n### Basic Deduplication\n\n```typescript\nawait inngest.send({\n  id: \"cart-checkout-completed-ed12c8bde\", // Unique per event type\n  name: \"storefront/cart.checkout.completed\",\n  data: {\n    cartId: \"ed12c8bde\",\n    items: [\"item1\", \"item2\"]\n  }\n});\n```\n\n### ID Best Practices\n\n```typescript\n// ✅ Good: Specific to event type and instance\nid: `invoice-paid-${invoiceId}`;\nid: `user-signup-${userId}-${timestamp}`;\nid: `order-shipped-${orderId}-${trackingNumber}`;\n\n// ❌ Bad: Generic IDs shared across event types\nid: invoiceId; // Could conflict with other events\nid: \"user-action\"; // Too generic\nid: customerId; // Same customer, different events\n```\n\n**Deduplication window:** 24 hours from first event reception\n\nSee **inngest-durable-functions** for idempotency configuration.\n\n## The `ts` Parameter for Delayed Delivery\n\n**When to use:** Schedule events for future processing or maintain event ordering.\n\n### Future Scheduling\n\n```typescript\nconst oneHourFromNow = Date.now() + 60 * 60 * 1000;\n\nawait inngest.send({\n  name: \"trial/reminder.send\",\n  ts: oneHourFromNow, // Deliver in 1 hour\n  data: {\n    userId: \"user_123\",\n    trialExpiresAt: \"2024-02-15T12:00:00Z\"\n  }\n});\n```\n\n### Maintaining Event Order\n\n```typescript\n// Events with timestamps are processed in chronological order\nconst events = [\n  {\n    name: \"user/action.performed\",\n    ts: 1640995200000, // Earlier\n    data: { action: \"login\" }\n  },\n  {\n    name: \"user/action.performed\",\n    ts: 1640995260000, // Later\n    data: { action: \"purchase\" }\n  }\n];\n\nawait inngest.send(events);\n```\n\n## Fan-Out Patterns\n\n**Use case:** One event triggers multiple independent functions for reliability and parallel processing.\n\n### Basic Fan-Out Implementation\n\n```typescript\n// Send single event\nawait inngest.send({\n  name: \"user/signup.completed\",\n  data: {\n    userId: \"user_123\",\n    email: \"user@example.com\",\n    plan: \"pro\"\n  }\n});\n\n// Multiple functions respond to same event\nconst sendWelcomeEmail = inngest.createFunction(\n  { id: \"send-welcome-email\", triggers: [{ event: \"user/signup.completed\" }] },\n  async ({ event, step }) => {\n    await step.run(\"send-email\", async () => {\n      return sendEmail({\n        to: event.data.email,\n        template: \"welcome\"\n      });\n    });\n  }\n);\n\nconst createTrialSubscription = inngest.createFunction(\n  { id: \"create-trial\", triggers: [{ event: \"user/signup.completed\" }] },\n  async ({ event, step }) => {\n    await step.run(\"create-subscription\", async () => {\n      return stripe.subscriptions.create({\n        customer: event.data.stripeCustomerId,\n        trial_period_days: 14\n      });\n    });\n  }\n);\n\nconst addToCrm = inngest.createFunction(\n  { id: \"add-to-crm\", triggers: [{ event: \"user/signup.completed\" }] },\n  async ({ event, step }) => {\n    await step.run(\"crm-sync\", async () => {\n      return crm.contacts.create({\n        email: event.data.email,\n        plan: event.data.plan\n      });\n    });\n  }\n);\n```\n\n### Fan-Out Benefits\n\n- **Independence:** Functions run separately; one failure doesn't affect others\n- **Parallel execution:** All functions run simultaneously\n- **Selective replay:** Re-run only failed functions\n- **Cross-service:** Trigger functions in different codebases/languages\n\n### Advanced Fan-Out with `waitForEvent`\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 orchestrateOnboarding = inngest.createFunction(\n  { id: \"orchestrate-onboarding\", triggers: [{ event: \"user/signup.completed\" }] },\n  async ({ event, step }) => {\n    // Fan out to multiple services\n    await step.sendEvent(\"fan-out\", [\n      { name: \"email/welcome.send\", data: event.data },\n      { name: \"subscription/trial.create\", data: event.data },\n      { name: \"crm/contact.add\", data: event.data }\n    ]);\n\n    // Wait for all to complete\n    const [emailResult, subResult, crmResult] = await Promise.all([\n      step.waitForEvent(\"email-sent\", {\n        event: \"email/welcome.sent\",\n        timeout: \"5m\",\n        if: `event.data.userId == async.data.userId`\n      }),\n      step.waitForEvent(\"subscription-created\", {\n        event: \"subscription/trial.created\",\n        timeout: \"5m\",\n        if: `event.data.userId == async.data.userId`\n      }),\n      step.waitForEvent(\"crm-synced\", {\n        event: \"crm/contact.added\",\n        timeout: \"5m\",\n        if: `event.data.userId == async.data.userId`\n      })\n    ]);\n\n    // Complete onboarding\n    await step.run(\"complete-onboarding\", async () => {\n      return completeUserOnboarding(event.data.userId);\n    });\n  }\n);\n```\n\nSee **inngest-steps** for additional patterns including `step.invoke`.\n\n## System Events\n\nInngest emits system events for function lifecycle monitoring:\n\n### Available System Events\n\n```typescript\n// Function execution events\n\"inngest/function.failed\"; // Function failed after retries\n\"inngest/function.finished\"; // Function finished - completed or failed\n\"inngest/function.cancelled\"; // Function cancelled before completion\n```\n\n### Handling Failed Functions\n\n```typescript\nconst handleFailures = inngest.createFunction(\n  { id: \"handle-failed-functions\", triggers: [{ event: \"inngest/function.failed\" }] },\n  async ({ event, step }) => {\n    const { function_id, run_id, error } = event.data;\n\n    await step.run(\"log-failure\", async () => {\n      logger.error(\"Function failed\", {\n        functionId: function_id,\n        runId: run_id,\n        error: error.message,\n        stack: error.stack\n      });\n    });\n\n    // Alert on critical function failures\n    if (function_id.includes(\"critical\")) {\n      await step.run(\"send-alert\", async () => {\n        return alerting.sendAlert({\n          title: `Critical function failed: ${function_id}`,\n          severity: \"high\",\n          runId: run_id\n        });\n      });\n    }\n\n    // Auto-retry certain failures\n    if (error.code === \"RATE_LIMIT_EXCEEDED\") {\n      await step.run(\"schedule-retry\", async () => {\n        return inngest.send({\n          name: \"retry/function.requested\",\n          ts: Date.now() + 5 * 60 * 1000, // Retry in 5 minutes\n          data: { originalRunId: run_id }\n        });\n      });\n    }\n  }\n);\n```\n\n## Sending Events\n\n### Client Setup\n\n```typescript\n// inngest/client.ts\nimport { Inngest } from \"inngest\";\n\nexport const inngest = new Inngest({\n  id: \"my-app\"\n});\n// You must set INNGEST_EVENT_KEY environment variable in production\n```\n\n### Single Event\n\n```typescript\nconst result = await inngest.send({\n  name: \"order/placed\",\n  data: {\n    orderId: \"ord_123\",\n    customerId: \"cus_456\",\n    amount: 2500,\n    items: [\n      { id: \"item_1\", quantity: 2 },\n      { id: \"item_2\", quantity: 1 }\n    ]\n  }\n});\n\n// Returns event IDs for tracking\nconsole.log(result.ids); // [\"01HQ8PTAESBZPBDS8JTRZZYY3S\"]\n```\n\n### Batch Events\n\n```typescript\nconst orderItems = await getOrderItems(orderId);\n\n// Convert to events\nconst events = orderItems.map((item) => ({\n  name: \"inventory/item.reserved\",\n  data: {\n    itemId: item.id,\n    orderId: orderId,\n    quantity: item.quantity,\n    warehouseId: item.warehouseId\n  }\n}));\n\n// Send all at once (up to 512kb)\nawait inngest.send(events);\n```\n\n### Sending from Functions\n\n```typescript\ninngest.createFunction(\n  { id: \"process-order\", triggers: [{ event: \"order/placed\" }] },\n  async ({ event, step }) => {\n    // Use step.sendEvent() instead of inngest.send() in functions\n    // for reliability and deduplication\n    await step.sendEvent(\"trigger-fulfillment\", {\n      name: \"fulfillment/order.received\",\n      data: {\n        orderId: event.data.orderId,\n        priority: event.data.customerTier === \"premium\" ? \"high\" : \"normal\"\n      }\n    });\n  }\n);\n```\n\n## Event Design Best Practices\n\n### Schema Versioning\n\n```typescript\n// Use version field to track schema changes\nawait inngest.send({\n  name: \"user/profile.updated\",\n  v: \"2024-01-15.1\", // Schema version\n  data: {\n    userId: \"user_123\",\n    changes: {\n      email: \"new@example.com\",\n      preferences: { theme: \"dark\" }\n    },\n    // New field in v2 schema\n    auditInfo: {\n      changedBy: \"user_456\",\n      reason: \"user_requested\"\n    }\n  }\n});\n```\n\n### Rich Context Data\n\n```typescript\n// Include enough context for all consumers\nawait inngest.send({\n  name: \"payment/charge.succeeded\",\n  data: {\n    // Primary identifiers\n    chargeId: \"ch_123\",\n    customerId: \"cus_456\",\n\n    // Amount details\n    amount: 2500,\n    currency: \"usd\",\n\n    // Context for different consumers\n    subscription: {\n      id: \"sub_789\",\n      plan: \"pro_monthly\"\n    },\n    invoice: {\n      id: \"inv_012\",\n      number: \"INV-2024-001\"\n    },\n\n    // Metadata for debugging\n    paymentMethod: {\n      type: \"card\",\n      last4: \"4242\",\n      brand: \"visa\"\n    },\n    metadata: {\n      source: \"stripe_webhook\",\n      environment: \"production\"\n    }\n  }\n});\n```\n\n**Event design principles:**\n\n1. **Self-contained:** Include all data consumers need\n2. **Immutable:** Never modify event schemas after sending\n3. **Traceable:** Include correlation IDs and audit trails\n4. **Actionable:** Provide enough context for business logic\n5. **Debuggable:** Include metadata for troubleshooting\n"
}

SHA-256 of public snapshot: 9f1279a2bcbc62db656350d195c31aa4e7c8638c79835eb72edc866b8392f120