inngest-events
inngest/inngest-skills
Design event-driven workflows with Inngest: idempotent event handling, fan-out patterns, and system event monitoring.
What is inngest-events?
Master Inngest event design and delivery patterns for building robust, decoupled services. Use this skill when designing event schemas, implementing idempotency with 24-hour deduplication windows, creating fan-out patterns (one event triggering multiple handlers), and handling at-least-once delivery from external sources like Stripe webhooks.
- Define event payloads with required name and data properties, plus optional id, ts, and v fields
- Implement idempotent event handling using event IDs with a 24-hour deduplication window
- Create fan-out patterns where a single event triggers multiple independent functions in parallel
- Schedule delayed event delivery using the ts parameter for future processing or maintaining event order
- Follow Object-Action naming conventions (domain/noun.verb) for clear, consistent event types
- Monitor function lifecycle with system events like inngest/function.failed and inngest/function.finished
How to install inngest-events
npx skills add https://github.com/inngest/inngest-skills --skill inngest-events- Inngest SDK installed and configured
- TypeScript environment (Python and Go require language-specific guidance)
How to use inngest-events
- 1.Define your event schema using the required name and data properties, plus optional id and ts fields
- 2.Choose event names following Object-Action pattern (e.g., billing/invoice.paid)
- 3.Add unique IDs to events that might be sent multiple times to enable 24-hour deduplication
- 4.Create multiple functions that respond to the same event name to implement fan-out patterns
- 5.Use the ts parameter to schedule events for future delivery or maintain event ordering
- 6.Monitor function lifecycle by listening to system events like inngest/function.failed
Use cases
- Send a billing event (e.g., invoice.paid) that triggers email notification, subscription creation, and CRM sync simultaneously
- Implement idempotent webhook handling from Stripe by using invoice IDs to prevent duplicate processing within 24 hours
- Schedule a trial reminder email to be delivered one hour from now using the ts parameter
- Orchestrate user onboarding by fanning out to multiple services and waiting for all to complete before finalizing
- Monitor failed function executions using inngest/function.failed system events for alerting and recovery
- Backend engineers designing event-driven architectures
- Full-stack developers implementing webhook integrations from external services
- Platform teams building decoupled microservices
- Developers handling at-least-once delivery guarantees from third-party APIs
inngest-events FAQ
Events are deduplicated for 24 hours from first reception. Use unique IDs per event type and instance (e.g., invoice-paid-${invoiceId}) to prevent duplicate processing.
Create multiple functions with the same event trigger. Each function runs independently in parallel. This fan-out pattern provides reliability since one function's failure doesn't affect others.
Use ts to schedule events for future delivery (e.g., send a reminder in 1 hour) or to maintain event ordering when processing multiple events with specific timestamps.
Use Object-Action pattern with domain prefixes: domain/noun.verb (e.g., billing/invoice.paid, user/profile.updated). Use past tense, dots for hierarchy, and consistent prefixes for related events.
Use event IDs for idempotency to handle at-least-once delivery from external sources. Inngest deduplicates identical event IDs within 24 hours, preventing duplicate processing.
Full 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-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.

inngest-middleware
Add cross-cutting concerns to Inngest functions: logging, error tracking, encryption, dependency injection, and custom telemetry.

inngest-setup
Set up Inngest durable execution in TypeScript projects with retry-safe handlers, background jobs, and scheduled tasks.

inngest-steps
Build durable workflows with Inngest steps—handle delays, events, and async work that survive restarts.

insforge
SDK integration for InsForge app features: database, auth, storage, functions, AI, realtime, email, and payments.

insforge-cli
Manage InsForge backend infrastructure, databases, deployments, and cloud services via CLI.