Trovella Wiki

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:

  1. Inserts chunk rows into the document_chunk table via withTenantContext (RLS-scoped)
  2. Syncs the same chunks to Typesense via indexChunks from @repo/search

Output

{
  success: true,
  sourceTable: string,
  sourceId: string,
  chunksIndexed: number,
  correlationId: string,  // UUID linking all chunks from this run
}

Dependencies

PackageImportPurpose
@repo/aichunkText, generateChunkContext, embedContentText processing and AI calls
@repo/dbdocumentChunk, withTenantContextDatabase writes with tenant isolation
@repo/loggercreateJobLoggerStructured logging for background jobs
@repo/searchindexChunksTypesense 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

  1. Create a new file in apps/web/src/inngest/functions/
  2. Import the shared client from ../client
  3. Use createJobLogger from @repo/logger (not console.log)
  4. Export the function from apps/web/src/inngest/functions/index.ts
  5. Register it in the functions array in apps/web/src/app/api/inngest/route.ts
  6. If the function needs tenant-scoped database access, use withTenantContext from @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 };
  },
);

On this page