| 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;
data: object;
};
Complete Schema
type EventPayload = {
name: string;
data: Record<string, any>;
id?: string;
ts?: number;
v?: string;
};
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
"billing/invoice.paid";
"user/profile.updated";
"order/item.shipped";
"ai/summary.completed";
"stripe/customer.created";
"intercom/conversation.assigned";
"slack/message.posted";
"payment";
"user_update";
"invoiceWasPaid";
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",
name: "storefront/cart.checkout.completed",
data: {
cartId: "ed12c8bde",
items: ["item1", "item2"],
},
});
ID Best Practices
id: `invoice-paid-${invoiceId}`;
id: `user-signup-${userId}-${timestamp}`;
id: `order-shipped-${orderId}-${trackingNumber}`;
id: invoiceId;
id: "user-action";
id: customerId;
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,
data: {
userId: "user_123",
trialExpiresAt: "2024-02-15T12:00:00Z",
},
});
Maintaining Event Order
const events = [
{
name: "user/action.performed",
ts: 1640995200000,
data: { action: "login" },
},
{
name: "user/action.performed",
ts: 1640995260000,
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
await inngest.send({
name: "user/signup.completed",
data: {
userId: "user_123",
email: "user@example.com",
plan: "pro",
},
});
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 }) => {
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 },
]);
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`,
}),
]);
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
"inngest/function.failed";
"inngest/function.finished";
"inngest/function.cancelled";
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,
});
});
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,
});
});
}
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,
data: { originalRunId: run_id },
});
});
}
},
);
Sending Events
Client Setup
import { Inngest } from "inngest";
export const inngest = new Inngest({
id: "my-app",
});
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 },
],
},
});
console.log(result.ids);
Batch Events
const orderItems = await getOrderItems(orderId);
const events = orderItems.map((item) => ({
name: "inventory/item.reserved",
data: {
itemId: item.id,
orderId: orderId,
quantity: item.quantity,
warehouseId: item.warehouseId,
},
}));
await inngest.send(events);
Sending from Functions
inngest.createFunction(
{ id: "process-order", triggers: [{ event: "order/placed" }] },
async ({ event, step }) => {
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
await inngest.send({
name: "user/profile.updated",
v: "2024-01-15.1",
data: {
userId: "user_123",
changes: {
email: "new@example.com",
preferences: { theme: "dark" },
},
auditInfo: {
changedBy: "user_456",
reason: "user_requested",
},
},
});
Rich Context Data
await inngest.send({
name: "payment/charge.succeeded",
data: {
chargeId: "ch_123",
customerId: "cus_456",
amount: 2500,
currency: "usd",
subscription: {
id: "sub_789",
plan: "pro_monthly",
},
invoice: {
id: "inv_012",
number: "INV-2024-001",
},
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
This Repository
These upstream Inngest instructions are vendored for agent tooling and
integration work in this monorepo.
Repository Triggers
Use this skill when inngest-events matches the current Inngest task. If the
right skill is unclear, start with docs/ai/skills/inngest/SKILL.md.
Repository Workflow
- Confirm whether the request is agent-tooling guidance or product runtime
integration.
- Use
inngest-brownfield-audit before changing existing app workflows or
fragile background work.
- Follow this upstream guidance under OpenSpec, root
AGENTS.md, repo
rulebooks, framework docs, and runtime evidence.
- Keep runtime packages, app code, migrations, and
INNGEST_* env
requirements out of agent-tooling-only changes.
Repository Checklist