Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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.
.claude/skills/asymmetric-al-inngest-events/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 93% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 271% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 115% | 0% |
| case-23 | ✗→✓ | ▲ Improved | 98% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 109% | 0% |
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.
Every Inngest event is a JSON object with required and optional properties:
typescripttype Event = { name: string; // Event type (triggers functions) data: object; // Payload data (any nested JSON) };
typescripttype 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 };
typescriptawait 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", }, }, });
Use the Object-Action pattern: domain/noun.verb
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
created, updated, failed)billing/invoice.paid)api/user.created, webhook/stripe.received)When to use IDs: Prevent duplicate processing when events might be sent multiple times.
typescriptawait inngest.send({ id: "cart-checkout-completed-ed12c8bde", // Unique per event type name: "storefront/cart.checkout.completed", data: { cartId: "ed12c8bde", items: ["item1", "item2"], }, });
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.
ts Parameter for Delayed DeliveryWhen to use: Schedule events for future processing or maintain event ordering.
typescriptconst 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", }, });
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);
Use case: One event triggers multiple independent functions for reliability and parallel processing.
typescript// 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, }); }); }, );
waitForEventIn expressions, event = the original triggering event, async = the new event being matched. See Expression Syntax Reference for full details.
typescriptconst 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.
Inngest emits system events for function lifecycle monitoring:
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
typescriptconst 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 }, }); }); } }, );
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
typescriptconst 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"]
typescriptconst 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);
typescriptinngest.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", }, }); }, );
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: "new@example.com", preferences: { theme: "dark" }, }, // New field in v2 schema auditInfo: { changedBy: "user_456", reason: "user_requested", }, }, });
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:
These upstream Inngest instructions are vendored for agent tooling and integration work in this monorepo.
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.
integration.
inngest-brownfield-audit before changing existing app workflows orfragile background work.
AGENTS.md, reporulebooks, framework docs, and runtime evidence.
INNGEST_* envrequirements out of agent-tooling-only changes.
or dependencies.
workflow behavior.
port.
docs/ai/skills/inngest/references/upstream.md.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 13,917 | 8,659 | -38% | 1 | 1 | 0% | 2,834 | 5,463 | +93% | 0 | 0 | — |
case-02 | pass→pass | 10,808 | 3,889 | -64% | 1 | 1 | 0% | 2,108 | 4,405 | +109% | 0 | 0 | — |
case-03 | pass→pass | 12,460 | 6,181 | -50% | 1 | 1 | 0% | 2,587 | 4,925 | +90% | 0 | 0 | — |
case-04 | pass→pass | 7,856 | 4,118 | -48% | 1 | 1 | 0% | 1,518 | 4,457 | +194% | 0 | 0 | — |
case-05 | pass→pass | 11,888 | 5,470 | -54% | 1 | 1 | 0% | 2,149 | 4,798 | +123% | 0 | 0 | — |
case-06 | pass→pass | 15,926 | 8,175 | -49% | 1 | 1 | 0% | 2,786 | 5,212 | +87% | 0 | 0 | — |
case-07 | pass→pass | 11,890 | 9,127 | -23% | 1 | 1 | 0% | 2,534 | 5,671 | +124% | 0 | 0 | — |
case-08 | pass→pass | 8,197 | 3,954 | -52% | 1 | 1 | 0% | 1,611 | 4,479 | +178% | 0 | 0 | — |
case-09 | pass→pass | 5,321 | 2,275 | -57% | 1 | 1 | 0% | 989 | 4,134 | +318% | 0 | 0 | — |
case-10 | pass→pass | 7,239 | 2,593 | -64% | 1 | 1 | 0% | 1,329 | 4,213 | +217% | 0 | 0 | — |
case-11 | pass→pass | 5,424 | 4,273 | -21% | 1 | 1 | 0% | 1,067 | 4,565 | +328% | 0 | 0 | — |
case-12 | fail→pass | 6,750 | 4,435 | -34% | 1 | 1 | 0% | 1,232 | 4,567 | +271% | 0 | 0 | — |
case-13 | pass→pass | 6,509 | 4,013 | -38% | 1 | 1 | 0% | 1,171 | 4,463 | +281% | 0 | 0 | — |
case-14 | fail→pass | 13,727 | 8,983 | -35% | 1 | 1 | 0% | 2,542 | 5,470 | +115% | 0 | 0 | — |
case-15 | pass→pass | 13,962 | 7,940 | -43% | 1 | 1 | 0% | 2,358 | 5,157 | +119% | 0 | 0 | — |
case-16 | pass→pass | 3,062 | 1,350 | -56% | 1 | 1 | 0% | 450 | 3,868 | +760% | 0 | 0 | — |
case-17 | pass→pass | 11,097 | 4,776 | -57% | 1 | 1 | 0% | 2,090 | 4,511 | +116% | 0 | 0 | — |
case-18 | pass→pass | 13,633 | 8,303 | -39% | 1 | 1 | 0% | 2,758 | 5,460 | +98% | 0 | 0 | — |
case-19 | pass→pass | 7,887 | 8,121 | +3% | 1 | 1 | 0% | 1,606 | 5,394 | +236% | 0 | 0 | — |
case-20 | pass→pass | 6,282 | 6,724 | +7% | 1 | 1 | 0% | 1,103 | 4,910 | +345% | 0 | 0 | — |
case-21 | pass→pass | 9,827 | 7,681 | -22% | 1 | 1 | 0% | 1,766 | 5,187 | +194% | 0 | 0 | — |
case-22 | pass→pass | 7,841 | 2,241 | -71% | 1 | 1 | 0% | 1,315 | 4,124 | +214% | 0 | 0 | — |
case-23 | fail→pass | 13,094 | 2,043 | -84% | 1 | 1 | 0% | 2,032 | 4,033 | +98% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +17 percentage points is the difference between those two pass rates over the 23 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.