Skip to main content
skill CX Codex

inngest-events — Claude Code skill: Inngest event design

Guides event-driven Inngest workflows: event naming, idempotency by id, fan-out patterns and system events.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add inngest/inngest-skills --skill inngest-events -a claude-code
Open on GitHub

What inngest-events does

The skill walks through the shape of an Inngest event. name and data are required, while id, ts and v are optional. Names follow an object-action pattern: billing/invoice.paid, user/profile.updated, stripe/customer.created. Verbs go in past tense, since an event records something that already happened.

A separate section covers id. Use it when the same event can arrive twice, which happens with webhooks from sources like Stripe. Deduplication runs for 24 hours from first reception, so an id should tie to both the event type and the instance: invoice-paid-${invoiceId}. A bare customerId will collide with other events from the same customer. The ts parameter pushes delivery into the future or keeps events in chronological order.

Fan-out means one trigger and several independent functions. A failing function does not drag the others down, and you can replay just that one. There is also a variant with step.sendEvent and step.waitForEvent when the fan-out needs to collect results. The skill targets TypeScript; for Python and Go the authors point to the Inngest docs, though the concepts carry over.

What it helps with

  • design an event schema with required and optional fields
  • name events with the object-action pattern
  • stop a webhook from being processed twice using id
  • split one trigger into several independent functions
  • schedule an event for later with ts

Example prompts for the agent

  • Design an invoice-paid event with name, data and a dedupe id
  • Fan out user/signup.completed into three independent functions
  • How do I make a Stripe webhook handler idempotent in Inngest?

How to install inngest-events

1Skills CLI (Vercel)

npx skills add inngest/inngest-skills --skill inngest-events -a claude-code

2Manually: put the files in place

~/.claude/skills/inngest-events/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/inngest-events/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about inngest-events

How do I install inngest-events in Claude?

Run npx skills add inngest/inngest-skills --skill inngest-events -a claude-code. The skill file then lives at ~/.claude/skills/inngest-events/SKILL.md.

What does inngest-events do?

It documents the Inngest event payload, naming rules, idempotency via id, the ts parameter and fan-out. The agent gets a reference instead of guessing field names.

Does the skill work with Python or Go?

It is written for TypeScript. For Python and Go the authors point to the Inngest documentation, but the event and deduplication concepts apply to any language.

Source text

SKILL.md · Apache-2.0
---
name: inngest-events
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.
---

# 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](https://www.inngest.com/llms.txt) 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 instance
id: `invoice-paid-${invoiceId}`;
id: `user-signup-${userId}-${timestamp}`;
id: `order-shipped-${orderId}-${trackingNumber}`;

// ❌ Bad: Generic IDs shared across event types
id: invoiceId; // Could conflict with other events
id: "user-action"; // Too generic
id: 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 order
const 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 event
await inngest.send({
  name: "user/signup.completed",
  data: {
    userId: "user_123",
    email: "[email protected]",
    plan: "pro"
  }
});

// Multiple functions respond to same event
const 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](../references/expressions.md) 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.ts
import { 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 tracking
console.log(result.ids); // ["01HQ8PTAESBZPBDS8JTRZZYY3S"]
```

### Batch Events

```typescript
const orderItems = await getOrderItems(orderId);

// Convert to events
const 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 changes
await 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 consumers
await 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

© inngest · Apache-2.0 · inngest/inngest-skills

Similar skills

Everything for Backend and APIs →

From the same set10

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor