| name | inngest-handler |
| description | Create and manage Inngest functions for reliable background jobs, workflows, and scheduled tasks. |
Inngest Function Handler Skill
This skill defines the standards for building durable, multi-step workflows using Inngest.
🚨 HARD RULES (Strictly Follow)
NO
setTimeout/setInterval:- ❌ Bad:
await new Promise(r => setTimeout(r, 1000)) - ✅ Good:
await step.sleep("wait-1s", "1s") - Reason: Serverless functions time out; Inngest sleeps persist for up to a year.
- ❌ Bad:
NO Side Effects Outside Steps:
- Any database write, API call, or non-deterministic logic (random, date) MUST be wrapped in
step.run(). - Reason: Inngest functions execute multiple times (memoization). Code outside steps runs every time.
- Any database write, API call, or non-deterministic logic (random, date) MUST be wrapped in
Deterministic Steps:
- Steps are memoized by their ID (1st arg). IDs must be unique and stable.
- Do not dynamically generate step IDs unless you know what you are doing (e.g., inside loops with index).
Return Data from Steps:
- If you need a value later, return it from the step.
- ❌ Bad:
let userId; await step.run(..., () => { userId = ... }) - ✅ Good:
const userId = await step.run(..., () => { return ... })
Core Patterns
1. Multi-Step Execution
Wrap all logic in steps to ensure retriability and resumability.
export const processOrder = inngest.createFunction(
{ id: "process-order" },
{ event: "shop/order.created" },
async ({ event, step }) => {
// 1. Step: Validate (Retriable)
const user = await step.run("get-user", async () => {
return await db.users.findById(event.data.userId);
});
// 2. Step: Sleep (Durable pause)
await step.sleep("wait-for-payment", "1h");
// 3. Step: Wait for Event (Human/System interaction)
const payment = await step.waitForEvent("wait-payment", {
event: "shop/payment.success",
match: "data.orderId",
timeout: "24h"
});
// 4. Step: Conditional Logic
if (!payment) {
await step.run("cancel-order", async () => { ... });
}
}
);
2. Parallelism
Run steps concurrently to speed up execution.
const [user, subscription] = await Promise.all([
step.run("fetch-user", () => db.users.find(...)),
step.run("fetch-sub", () => stripe.subscriptions.retrieve(...))
]);
3. Working with Loops
Inside loops, ensure step IDs are unique.
const items = event.data.items;
for (const item of items) {
// Use dynamic ID to ensure uniqueness per item
await step.run(`process-item-${item.id}`, async () => {
await processItem(item);
});
}
Configuration & Flow Control
Rate Limiting & Throttling
Prevent overwhelming 3rd party APIs.
inngest.createFunction({
id: "sync-crm",
// Max 10 requests per minute per user
rateLimit: { limit: 10, period: "1m", key: "event.data.userId" },
// Drop events if queue is full
throttle: { limit: 5, period: "1s" }
}, ...);
Debounce
Process only the latest event in a window (e.g., search indexing).
inngest.createFunction({
id: "index-product",
// Wait 10s for more events; only run with the latest data
debounce: { period: "10s", key: "event.data.productId" }
}, ...);
Priority
Prioritize specific events (e.g., Paid users).
inngest.createFunction({
id: "generate-report",
// High number = High priority
priority: { run: "event.data.plan === 'enterprise' ? 100 : 0" }
}, ...);
Error Handling
Automatic Retries
Inngest retries steps automatically on error (default ~4-5 times with backoff).
- Customize:
{ retries: 10 }in config.
Non-Retriable Errors
Stop execution immediately if the error is fatal (e.g., 400 Bad Request).
import { NonRetriableError } from "inngest";
await step.run("validate", async () => {
if (!isValid) throw new NonRetriableError("Invalid payload");
});
Failure Handlers (Rollbacks)
Execute cleanup logic if the function fails after all retries.
export const riskyFunc = inngest.createFunction(
{
id: "risky-transfer",
// Runs if main handler fails
onFailure: async ({ error, event, step }) => {
await step.run("rollback-funds", async () => {
await reverseTransfer(event.data.transferId);
});
await step.run("notify-admin", async () => {
await sendAlert(`Transfer failed: ${error.message}`);
});
}
},
{ event: "bank/transfer.init" },
async ({ step }) => { /* ... */ }
);
Registration
MANDATORY: All functions must be imported and exported in src/lib/inngest/functions/index.ts.