How to install inngest-events
npx skills add https://github.com/inngest/inngest-skills --skill inngest-eventsFull instructions (SKILL.md)
Source of truth, from inngest/inngest-skills.
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 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
type Event = {
name: string; // Event type (triggers functions)
data: object; // Payload data (any nested JSON)
};
Complete Schema
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
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
// ✅ 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
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
// ✅ 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
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
// 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
// Send single event
await inngest.send({
name: "user/signup.completed",
data: {
userId: "user_123",
email: "user@example.com",
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 for full details.
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
// 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
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
// 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
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
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
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
// 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: "new@example.com",
preferences: { theme: "dark" }
},
// New field in v2 schema
auditInfo: {
changedBy: "user_456",
reason: "user_requested"
}
}
});
Rich Context Data
// 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:
- Self-contained: Include all data consumers need
- Immutable: Never modify event schemas after sending
- Traceable: Include correlation IDs and audit trails
- Actionable: Provide enough context for business logic
- Debuggable: Include metadata for troubleshooting
Related skills
More from inngest/inngest-skills and the wider catalog.
inngest-durable-functions
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.
inngest-steps
Use when implementing delays that must survive process restarts (e.g., 24-hour cart abandonment, scheduled follow-ups), waiting for human approval or external events with timeouts (review gates, webhook callbacks, async API completion), polling external services without losing state on crashes, calling other functions and awaiting their results, memoizing expensive operations so they don't re-run on retry, or running async work in parallel inside a workflow. Covers Inngest step methods: step.run, step.sleep, step.waitForEvent, step.waitForSignal, step.sendEvent, step.invoke, step.ai, plus patterns for loops and parallel execution.
inngest-setup
Use when adding durable execution to a TypeScript project — building retry-safe webhook handlers, background jobs that survive crashes, scheduled tasks, or long-running workflows that outlive a single request. Covers Inngest SDK installation, client config, environment variables, serve endpoints (Next.js, Express, Hono, Fastify), connect-as-worker mode, and the local dev server.
inngest-middleware
Use when adding cross-cutting concerns to durable functions — structured logging or tracing across all functions, error tracking with Sentry, payload encryption for sensitive data, dependency injection of clients (DB, Stripe, etc.) into function handlers, custom telemetry, or behavior that should apply uniformly across many functions. Covers Inngest middleware lifecycle, creating custom middleware, dependencyInjectionMiddleware, @inngest/middleware-encryption, @inngest/middleware-sentry, and custom middleware patterns.
inngest-flow-control
Use when handling external API rate limits (e.g., OpenAI 429s, HubSpot or Stripe rate limits), preventing duplicate work from rapid event bursts (debouncing user actions), spreading load over time, ensuring per-tenant fairness, processing events in batches, limiting concurrent runs of the same operation, or assigning priority to important runs. Covers Inngest flow control: concurrency limits with keys, throttling, rate limiting, debounce, priority, singleton, and event batching.