Inngest Events

by inngest082798a627bbNo license28 starsListed Oct 8, 2026Updated Oct 8, 2026Repository updated 10 days ago

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.

AI-generated overview

Guides Inngest event design: payload schema, naming, idempotency IDs, fan-out patterns, and system events.

What it does
Explains the Inngest event payload format, including required name and data fields plus optional id, ts, and v properties. Covers naming conventions, deduplication IDs with a 24-hour window, delayed delivery via ts, fan-out patterns where one event triggers multiple functions, and system events such as inngest/function.failed. Produces design guidance and TypeScript code examples rather than runnable artifacts.
When to use it
Use when designing event-driven workflows, decoupling services, or implementing fan-out and idempotent event handling. Also relevant when consuming at-least-once delivery from external sources such as Stripe webhooks.
Requirements
No scripts or packages ship with the skill; it is instructions only. Examples assume TypeScript and the Inngest SDK, and production use requires the INNGEST_EVENT_KEY environment variable.

Inngest Events

Master 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.

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.

Event Payload Format

Every Inngest event is a JSON object with required and optional properties:

Required Properties

typescript
type Event = {  name: string; // Event type (triggers functions)  data: object; // Payload data (any nested JSON)};

Complete Schema

typescript
type EventPayload = {  name: string; // Required: event type  data: Record<string, any>; // Required: event data  id?: string; // Optional: deduplication ID  ts?: number; // Optional: timestamp (Unix ms)  v?: string; // Optional: schema version};

Basic Event Example

typescript
await inngest.send({  name: "billing/invoice.paid",  data: {    customerId: "cus_NffrFeUfNV2Hib",    invoiceId: "in_1J5g2n2eZvKYlo2C0Z1Z2Z3Z",    userId: "user_03028hf09j2d02",    amount: 1000,    metadata: {      accountId: "acct_1J5g2n2eZvKYlo2C0Z1Z2Z3Z",      accountName: "Acme.ai"    }  }});

Event Naming Conventions

Use the Object-Action pattern: domain/noun.verb

Recommended Patterns

typescript
// ✅ Good: Clear object-action pattern"billing/invoice.paid";"user/profile.updated";"order/item.shipped";"ai/summary.completed";
// ✅ Good: Domain prefixes for organization"stripe/customer.created";"intercom/conversation.assigned";"slack/message.posted";
// ❌ Avoid: Unclear or inconsistent"payment"; // What happened?"user_update"; // Use dots, not underscores"invoiceWasPaid"; // Too verbose

Naming Guidelines

  • Past tense: Events describe what happened (created, updated, failed)
  • Dot notation: Use dots for hierarchy (billing/invoice.paid)
  • Prefixes: Group related events (api/user.created, webhook/stripe.received)
  • Consistency: Establish patterns and stick to them

Event IDs and Idempotency

When to use IDs: Prevent duplicate processing when events might be sent multiple times.

Basic Deduplication

typescript
await inngest.send({  id: "cart-checkout-completed-ed12c8bde", // Unique per event type  name: "storefront/cart.checkout.completed",  data: {    cartId: "ed12c8bde",    items: ["item1", "item2"]  }});

ID Best Practices

typescript
// ✅ Good: Specific to event type and instanceid: `invoice-paid-${invoiceId}`;id: `user-signup-${userId}-${timestamp}`;id: `order-shipped-${orderId}-${trackingNumber}`;
// ❌ Bad: Generic IDs shared across event typesid: invoiceId; // Could conflict with other eventsid: "user-action"; // Too genericid: customerId; // Same customer, different events

Deduplication window: 24 hours from first event reception

See inngest-durable-functions for idempotency configuration.

The ts Parameter for Delayed Delivery

When to use: Schedule events for future processing or maintain event ordering.

Future Scheduling

typescript
const oneHourFromNow = Date.now() + 60 * 60 * 1000;
await inngest.send({  name: "trial/reminder.send",  ts: oneHourFromNow, // Deliver in 1 hour  data: {    userId: "user_123",    trialExpiresAt: "2024-02-15T12:00:00Z"  }});

Maintaining Event Order

typescript
// Events with timestamps are processed in chronological orderconst events = [  {    name: "user/action.performed",    ts: 1640995200000, // Earlier    data: { action: "login" }  },  {    name: "user/action.performed",    ts: 1640995260000, // Later    data: { action: "purchase" }  }];
await inngest.send(events);

Fan-Out Patterns

Use case: One event triggers multiple independent functions for reliability and parallel processing.

Basic Fan-Out Implementation

typescript
// Send single eventawait inngest.send({  name: "user/signup.completed",  data: {    userId: "user_123",    email: "[email protected]",    plan: "pro"  }});
// Multiple functions respond to same eventconst sendWelcomeEmail = inngest.createFunction(  { id: "send-welcome-email", triggers: [{ event: "user/signup.completed" }] },  async ({ event, step }) => {    await step.run("send-email", async () => {      return sendEmail({        to: event.data.email,        template: "welcome"      });    });  });
const createTrialSubscription = inngest.createFunction(  { id: "create-trial", triggers: [{ event: "user/signup.completed" }] },  async ({ event, step }) => {    await step.run("create-subscription", async () => {      return stripe.subscriptions.create({        customer: event.data.stripeCustomerId,        trial_period_days: 14      });    });  });
const addToCrm = inngest.createFunction(  { id: "add-to-crm", triggers: [{ event: "user/signup.completed" }] },  async ({ event, step }) => {    await step.run("crm-sync", async () => {      return crm.contacts.create({        email: event.data.email,        plan: event.data.plan      });    });  });

Fan-Out Benefits

  • Independence: Functions run separately; one failure doesn't affect others
  • Parallel execution: All functions run simultaneously
  • Selective replay: Re-run only failed functions
  • Cross-service: Trigger functions in different codebases/languages

Advanced Fan-Out with waitForEvent

In expressions, event = the original triggering event, async = the new event being matched. See Expression Syntax Reference for full details.

typescript
const orchestrateOnboarding = inngest.createFunction(  { id: "orchestrate-onboarding", triggers: [{ event: "user/signup.completed" }] },  async ({ event, step }) => {    // Fan out to multiple services    await step.sendEvent("fan-out", [      { name: "email/welcome.send", data: event.data },      { name: "subscription/trial.create", data: event.data },      { name: "crm/contact.add", data: event.data }    ]);
    // Wait for all to complete    const [emailResult, subResult, crmResult] = await Promise.all([      step.waitForEvent("email-sent", {        event: "email/welcome.sent",        timeout: "5m",        if: `event.data.userId == async.data.userId`      }),      step.waitForEvent("subscription-created", {        event: "subscription/trial.created",        timeout: "5m",        if: `event.data.userId == async.data.userId`      }),      step.waitForEvent("crm-synced", {        event: "crm/contact.added",        timeout: "5m",        if: `event.data.userId == async.data.userId`      })    ]);
    // Complete onboarding    await step.run("complete-onboarding", async () => {      return completeUserOnboarding(event.data.userId);    });  });

See inngest-steps for additional patterns including step.invoke.

System Events

Inngest emits system events for function lifecycle monitoring:

Available System Events

typescript
// Function execution events"inngest/function.failed"; // Function failed after retries"inngest/function.finished"; // Function finished - completed or failed"inngest/function.cancelled"; // Function cancelled before completion

Handling Failed Functions

typescript
const handleFailures = inngest.createFunction(  { id: "handle-failed-functions", triggers: [{ event: "inngest/function.failed" }] },  async ({ event, step }) => {    const { function_id, run_id, error } = event.data;
    await step.run("log-failure", async () => {      logger.error("Function failed", {        functionId: function_id,        runId: run_id,        error: error.message,        stack: error.stack      });    });
    // Alert on critical function failures    if (function_id.includes("critical")) {      await step.run("send-alert", async () => {        return alerting.sendAlert({          title: `Critical function failed: ${function_id}`,          severity: "high",          runId: run_id        });      });    }
    // Auto-retry certain failures    if (error.code === "RATE_LIMIT_EXCEEDED") {      await step.run("schedule-retry", async () => {        return inngest.send({          name: "retry/function.requested",          ts: Date.now() + 5 * 60 * 1000, // Retry in 5 minutes          data: { originalRunId: run_id }        });      });    }  });

Sending Events

Client Setup

typescript
// inngest/client.tsimport { Inngest } from "inngest";
export const inngest = new Inngest({  id: "my-app"});// You must set INNGEST_EVENT_KEY environment variable in production

Single Event

typescript
const result = await inngest.send({  name: "order/placed",  data: {    orderId: "ord_123",    customerId: "cus_456",    amount: 2500,    items: [      { id: "item_1", quantity: 2 },      { id: "item_2", quantity: 1 }    ]  }});
// Returns event IDs for trackingconsole.log(result.ids); // ["01HQ8PTAESBZPBDS8JTRZZYY3S"]

Batch Events

typescript
const orderItems = await getOrderItems(orderId);
// Convert to eventsconst events = orderItems.map((item) => ({  name: "inventory/item.reserved",  data: {    itemId: item.id,    orderId: orderId,    quantity: item.quantity,    warehouseId: item.warehouseId  }}));
// Send all at once (up to 512kb)await inngest.send(events);

Sending from Functions

typescript
inngest.createFunction(  { id: "process-order", triggers: [{ event: "order/placed" }] },  async ({ event, step }) => {    // Use step.sendEvent() instead of inngest.send() in functions    // for reliability and deduplication    await step.sendEvent("trigger-fulfillment", {      name: "fulfillment/order.received",      data: {        orderId: event.data.orderId,        priority: event.data.customerTier === "premium" ? "high" : "normal"      }    });  });

Event Design Best Practices

Schema Versioning

typescript
// Use version field to track schema changesawait inngest.send({  name: "user/profile.updated",  v: "2024-01-15.1", // Schema version  data: {    userId: "user_123",    changes: {      email: "[email protected]",      preferences: { theme: "dark" }    },    // New field in v2 schema    auditInfo: {      changedBy: "user_456",      reason: "user_requested"    }  }});

Rich Context Data

typescript
// Include enough context for all consumersawait inngest.send({  name: "payment/charge.succeeded",  data: {    // Primary identifiers    chargeId: "ch_123",    customerId: "cus_456",
    // Amount details    amount: 2500,    currency: "usd",
    // Context for different consumers    subscription: {      id: "sub_789",      plan: "pro_monthly"    },    invoice: {      id: "inv_012",      number: "INV-2024-001"    },
    // Metadata for debugging    paymentMethod: {      type: "card",      last4: "4242",      brand: "visa"    },    metadata: {      source: "stripe_webhook",      environment: "production"    }  }});

Event design principles:

  1. Self-contained: Include all data consumers need
  2. Immutable: Never modify event schemas after sending
  3. Traceable: Include correlation IDs and audit trails
  4. Actionable: Provide enough context for business logic
  5. Debuggable: Include metadata for troubleshooting

Source and attribution

Source:inngest/inngest-skillsinskills/inngest-eventsat commit082798a

License: No license

Content belongs to its original authors. SourceWeft indexes it from a public repository.

Report or request removal