Inngest Function Best Practices
File Organization
- •Create separate files - Each Inngest function should be created in its own file within the
inngest/functionsdirectory, NOT in the maininngest/functions.tsfile. - •Use nested directories by domain - Organize functions in subdirectories based on their domain/feature area:
code
inngest/ ├── functions/ │ ├── campaign/ │ │ ├── enrich-company.ts │ │ ├── send-email.ts │ │ └── track-engagement.ts │ ├── user/ │ │ ├── onboarding.ts │ │ └── send-welcome-email.ts │ └── payment/ │ ├── process-charge.ts │ └── handle-refund.ts
- •Naming convention - Use descriptive kebab-case filenames that clearly indicate the function's purpose (e.g.,
enrich-company.ts,process-payment.ts) - •Export pattern - Export the function as the default export from each file
- •Import in functions.ts - Import and aggregate all functions in the main
inngest/functions.tsfile:typescript// inngest/functions.ts import enrichCompany from './functions/campaign/enrich-company'; import sendEmail from './functions/campaign/send-email'; import onboardUser from './functions/user/onboarding'; export default [ enrichCompany, sendEmail, onboardUser, // ... other functions ];
Workflow Step Design
step.run() Usage
- •Wrap all operations - Every operation that should be retried or tracked should be wrapped in
step.run() - •Descriptive step names - Use clear, action-oriented names (e.g.,
"fetch-user-data","send-email","update-database") - •One responsibility per step - Each
step.run()should perform a single logical operation - •Return meaningful data - Always return data from steps that subsequent steps might need
Error Handling
- •Explicit error handling - Wrap
step.run()calls in try-catch blocks when you need custom error handling - •Use NonRetriableError - For validation errors or business logic failures that shouldn't retry:
typescript
import { NonRetriableError } from "inngest"; throw new NonRetriableError("User not found"); - •Let retriable errors bubble - For temporary failures (network, timeouts), let errors propagate naturally for automatic retry
Return Values and Flow Control
- •Return success/failure objects - Return structured data indicating outcome:
typescript
return { success: true, data: result }; // or return { success: false, error: "reason" }; - •Use step results in subsequent steps - Access previous step results in later steps:
typescript
const user = await step.run("get-user", async () => { return await getUser(userId); }); await step.run("send-email", async () => { return await sendEmail(user.email); });
Retry and Timing Configuration
- •
Configure retries - Set retry attempts based on operation criticality:
typescriptinngest.createFunction( { id: "my-function", retries: 3 // or configure per step }, { event: "my.event" }, async ({ event, step }) => { } ) - •
Use step.sleep() for delays - Add intentional delays between operations:
typescriptawait step.sleep("wait-before-retry", "5s");
Event Patterns
- •
Type-safe events - Define event types for better DX:
typescripttype MyEvent = { name: "user.created"; data: { userId: string; email: string }; }; - •
Event-driven workflows - Chain functions by triggering subsequent events:
typescriptawait step.run("trigger-next-step", async () => { await inngest.send({ name: "user.onboarding.start", data: { userId: event.data.userId } }); });
Complete Example Pattern
typescript
import { inngest } from "@/inngest/client";
import { NonRetriableError } from "inngest";
export default inngest.createFunction(
{
id: "process-user-signup",
retries: 2
},
{ event: "user.signup" },
async ({ event, step }) => {
// Step 1: Validate input
const validation = await step.run("validate-input", async () => {
if (!event.data.email) {
throw new NonRetriableError("Email is required");
}
return { valid: true };
});
// Step 2: Create user record
const user = await step.run("create-user", async () => {
return await createUserInDB(event.data);
});
// Step 3: Send welcome email (with retry)
await step.run("send-welcome-email", async () => {
return await sendEmail({
to: user.email,
template: "welcome"
});
});
// Step 4: Trigger onboarding workflow
await step.run("trigger-onboarding", async () => {
await inngest.send({
name: "user.onboarding.start",
data: { userId: user.id }
});
});
return { success: true, userId: user.id };
}
);
Key Reminders
- •✅ Always use
step.run()for operations that should be tracked and retried - •✅ Always return meaningful data from each step
- •✅ Always use NonRetriableError for validation/business logic failures
- •✅ Always organize functions in separate files in the
functionsdirectory - •✅ Consider using
step.sleep()for rate limiting or scheduled delays - •✅ Consider breaking complex workflows into multiple chained functions via events