Monitor and operate the Voiceflow background jobs that process uploaded audio recordings and run routine maintenance. Background jobs are handled through Inngest, mounted at /api/inngest.

Pipeline Execution Steps#

Feedback processing begins when the feedback/uploaded event fires after an upload is completed. The processFeedback function manages processing through three discrete steps:

claim ─> speech (Sarvam x2) ─> sentiment (LLM) ─> PostgreSQL
  1. claim: The function takes ownership of the feedback record by updating its status to PROCESSING and clearing any existing errorCode. If the record is already moved beyond UPLOADED or PROCESSING, the step skips further execution.
  2. speech: The function checks whether a transcription record already exists. If missing, it downloads the audio payload from storage using getObjectBytes, invokes transcribe, and stores the resulting transcript in the database.
  3. sentiment: The function passes the English transcript to analyseSentiment. It updates the database in a transaction that marks the feedback row as COMPLETED with a completedAt timestamp.

Retries, Throttling, and Failures#

The processFeedback job uses the following execution constraints:

  • Concurrency: Restricted to a limit of 1 keyed by event.data.feedbackId. Duplicate or rescued events wait until earlier runs finish, then stop if the record has already moved on.
  • Throttling: Capped at 25 calls per 1m to respect upstream Sarvam limits.
  • Retries: Up to 3 retries for transient errors.
  • Failure Handling: If the job runs out of retries or encounters non-retriable pipeline errors (SPEECH_REJECTED, NO_SPEECH, or ANALYSIS_INVALID), the feedback row status is updated to FAILED and sets the errorCode. Other unhandled failures record the error code PROCESSING_FAILED.

Speech Transcription with Sarvam#

The transcribe service interacts with Sarvam's Speech-to-Text API (https://api.sarvam.ai/speech-to-text) using the saaras:v4 model.

For each recording, transcribe performs two parallel calls using the provided audio buffer and MIME type:

  1. Original transcript: Runs Sarvam in transcribe mode to transcribe the speech in the indicated language (or "unknown" if no languageHint is set).
  2. English transcript: Runs Sarvam in translate mode to generate an English translation.

Each call enforces an abort timeout of 20 seconds.

Error Handling in Transcription#

  • Empty Transcripts: If either the original or English transcript is empty after trimming whitespace, the service throws a non-retriable NO_SPEECH error.
  • Client Errors (HTTP 400, 403, 422): Bad requests, invalid authentication keys, or unprocessable audio throw a non-retriable SPEECH_REJECTED error.
  • Rate Limits (HTTP 429): Throws a RetryAfterError requesting a 30-second backoff (SPEECH_RATE_LIMITED).
  • Network Errors and Timeouts: Throws SPEECH_UNAVAILABLE to trigger standard Inngest retries.

Because transcripts are stored immediately during the speech step, downstream failures during sentiment analysis do not re-run or re-bill Sarvam calls on retry.

Sentiment Analysis#

The analyseSentiment service processes the generated English text using the OpenRouter AI SDK provider and the model specified in SENTIMENT_MODEL (defaulting to openai/gpt-4o-mini). It runs with prompt version v3.

To protect against prompt injection, the transcript is placed inside <feedback> tags and declared strictly as input data within the system instructions.

Structured Output and Schema#

The LLM returns an object validated against a Zod schema containing:

  • label: Enum of "POSITIVE", "NEUTRAL", "NEGATIVE", or "MIXED".
  • score: A polarity value between -1 and 1. An internal helper (alignScore) ensures the sign matches the label (positive values for "POSITIVE", negative values for "NEGATIVE").
  • summary: A single sentence summarizing the feedback, capped at 25 words.
  • topics: An array of identified topics constrained to: "food quality", "delivery time", "packaging", "price", "staff behaviour", "hygiene", "order accuracy", "missing items", "portion size", or "other".
  • urgent: A boolean flag set to true for complaints related to health, safety, or serious issues.

If the model response fails schema generation (NoObjectGeneratedError), the step logs the error and raises a non-retriable ANALYSIS_INVALID error.

Scheduled Housekeeping Cleanup#

The housekeeping function runs on a scheduled cron trigger every 10 minutes (*/10 * * * *) with up to 2 retries.

It runs three tasks sequentially as independent steps so that a failure in one step does not repeat the others:

expire-abandoned-uploads ─> requeue-stuck-uploads ─> delete-unneeded-audio

Cleanup Tasks#

Step / Service FunctionExecution ThresholdBatch SizeAction Taken
expireAbandonedUploadsstatus = "UPLOADING" older than 15 minutes (UPLOAD_TIMEOUT_MS)All matching rowsMarks rows with status EXPIRED and error code UPLOAD_EXPIRED. Uses a single conditional query to prevent races with customer confirmations.
requeueStuckUploadsstatus = "UPLOADED" unchanged for more than 5 minutes (STUCK_AFTER_MS)100 rows (BATCH_SIZE)Re-emits feedback/uploaded events to Inngest using an event ID formatted as {id}-{attempt}-rescue-{timestamp} to restart stalled processing.
deleteUnneededAudioAudio not yet deleted (audioDeletedAt is null) and status is EXPIRED or record is older than 30 days (AUDIO_RETENTION_DAYS)100 rows (BATCH_SIZE)Deletes audio files from storage via deleteObjects, then sets audioDeletedAt to the current timestamp. Transcripts and sentiment rows remain in the database.

If deleteUnneededAudio fails during file deletion, the database rows remain unflagged (audioDeletedAt: null), causing the operation to retry automatically on the next scheduled run.