Job Definitions
Detailed walkthrough of each Inngest function -- purpose, steps, inputs, and outputs.
All Inngest functions live in apps/web/src/inngest/functions/ and are registered in the /api/inngest route handler. Each function uses the shared Inngest client from apps/web/src/inngest/client.ts.
Inngest Client
The client is a single instance with the application ID "trovella":
import { Inngest } from "inngest";
export const inngest = new Inngest({ id: "trovella" });
Function Registration
All functions are exported from the barrel file apps/web/src/inngest/functions/index.ts and registered in the Next.js API route:
import { serve } from "inngest/next";
import { inngest } from "@/inngest/client";
import { indexContent, welcomeEmail } from "@/inngest/functions";
export const { GET, POST, PUT } = serve({
client: inngest,
functions: [welcomeEmail, indexContent],
});
The serve() function exposes GET (introspection), POST (event ingestion and step execution), and PUT (function registration) endpoints.
index-content
File: apps/web/src/inngest/functions/index-content.ts
Trigger: search/content.created
Retries: 2
Concurrency: 5
This is the primary production function. It powers the write path of the hybrid search pipeline by processing newly stored research content into searchable chunks.
Input Event
interface IndexContentEvent {
data: {
sourceTable: "research_artifact" | "research_output" | "extraction_result";
sourceId: string;
title: string;
content: string; // full text to chunk and embed
organizationId: string; // tenant scoping
userId: string;
artifactType?: string; // faceted search
mediaType?: string; // faceted search
planId?: string; // faceted search
};
}
Steps
Each step is a durable checkpoint. If the function fails at step 3, re-execution starts at step 3 (steps 1 and 2 are not repeated).
Step 1: chunk-content -- Splits the input text using a recursive character splitter (chunkText from @repo/ai). Chunk size is 2048 characters (~512 tokens) with 200 characters (~50 tokens) of overlap. Returns an array of TextChunk objects.
Step 2: generate-context -- For multi-chunk documents, calls Claude Haiku (generateChunkContext from @repo/ai) to generate a 2-3 sentence contextual prefix for each chunk. Single-chunk documents skip this step. The contextual prefix is prepended to the original text to form embeddedText.
Step 3: embed-chunks -- Calls Gemini (embedContent from @repo/ai) to produce 1536-dimensional vectors for all chunks. Uses task type RETRIEVAL_DOCUMENT and feature tag hybrid-search-index.
Step 4: store-and-sync -- Dual-writes to both data stores:
- Inserts chunk rows into the
document_chunktable viawithTenantContext(RLS-scoped) - Syncs the same chunks to Typesense via
indexChunksfrom@repo/search
Output
{
success: true,
sourceTable: string,
sourceId: string,
chunksIndexed: number,
correlationId: string, // UUID linking all chunks from this run
}
Dependencies
| Package | Import | Purpose |
|---|---|---|
@repo/ai | chunkText, generateChunkContext, embedContent | Text processing and AI calls |
@repo/db | documentChunk, withTenantContext | Database writes with tenant isolation |
@repo/logger | createJobLogger | Structured logging for background jobs |
@repo/search | indexChunks | Typesense sync for keyword search |
welcome-email
File: apps/web/src/inngest/functions/welcome-email.ts
Trigger: auth/user.created
Retries: 3
Concurrency: default (unlimited)
A placeholder function demonstrating the step-based retry pattern. It will be connected to Resend + React Email when email sending is implemented.
Steps
Step 1: lookup-user -- Currently a stub that extracts user data from the event payload.
Step 2: send-welcome-email -- Currently a stub that logs the intent to send.
Output
{ success: true, userId: string }
Adding a New Function
- Create a new file in
apps/web/src/inngest/functions/ - Import the shared client from
../client - Use
createJobLoggerfrom@repo/logger(notconsole.log) - Export the function from
apps/web/src/inngest/functions/index.ts - Register it in the
functionsarray inapps/web/src/app/api/inngest/route.ts - If the function needs tenant-scoped database access, use
withTenantContextfrom@repo/db(see Query Patterns)
import { createJobLogger } from "@repo/logger";
import { inngest } from "../client";
export const myNewJob = inngest.createFunction(
{
id: "my-new-job",
triggers: [{ event: "domain/entity.action" }],
retries: 2,
},
async ({ event, step }) => {
const logger = createJobLogger("my-new-job", {
runId: event.id,
});
const result = await step.run("step-name", async () => {
logger.info("Doing work");
// your logic here
return { data: "value" };
});
return { success: true };
},
);ADR-006: Background Jobs -- Inngest (Self-Hosted)
Decision record for choosing Inngest as the durable execution engine over Temporal, BullMQ, and DIY approaches.
Event Patterns
How events flow from MCP tools through Inngest to background functions -- emitter design, event naming, and decoupling strategy.