---
title: "Sentiment Analysis"
description: "Analyze customer feedback in bulk with nua.list()."
canonical: "https://nuabase.com/docs/examples/sentiment-analysis/"
---

> Documentation index: https://nuabase.com/llms.txt

This example analyzes customer feedback in bulk with `nua.list()`.

## Problem

You receive hundreds of customer reviews daily and need to:

1. Classify each review's sentiment
2. Extract key topics mentioned
3. Identify actionable feedback
4. Prioritize responses

## Solution

```typescript
import { Nua, z } from 'nuabase';
import { findLocalAgent } from 'nuabase/local-agent';

const nua = Nua.direct({ localAgent: await findLocalAgent() });

// Define the analysis schema
const SentimentSchema = z.object({
  sentiment: z.enum(['positive', 'neutral', 'negative']),
  score: z.number().min(-1).max(1),
  topics: z.array(z.string()).max(3),
  isActionable: z.boolean(),
  actionRequired: z.string().optional(),
  responseUrgency: z.enum(['none', 'low', 'medium', 'high']),
});

// Sample reviews
const reviews = [
  {
    id: 'rev-001',
    text: 'Absolutely love this product! The customer service was exceptional. Already recommended to friends.',
    source: 'app-store',
    date: '2024-01-15'
  },
  {
    id: 'rev-002',
    text: 'App crashes every time I try to export. Very frustrating. Been waiting 3 days for support response.',
    source: 'support-ticket',
    date: '2024-01-15'
  },
  {
    id: 'rev-003',
    text: 'It works fine for basic tasks. Nothing special but gets the job done.',
    source: 'app-store',
    date: '2024-01-14'
  },
  {
    id: 'rev-004',
    text: 'The new update broke my workflow. I rely on this for my business. Please fix ASAP!',
    source: 'twitter',
    date: '2024-01-15'
  },
  {
    id: 'rev-005',
    text: 'Great value for the price. The team feature is a game changer for our agency.',
    source: 'g2-review',
    date: '2024-01-13'
  }
];

async function analyzeReviews() {
  const result = await nua.list(
    `Analyze each customer review for sentiment and actionability.

For each review:
- Determine overall sentiment (positive/neutral/negative)
- Assign a score from -1 (very negative) to 1 (very positive)
- Extract up to 3 main topics mentioned
- Determine if action is required (bug report, complaint needing response, etc.)
- If actionable, describe what action is needed
- Assess response urgency based on sentiment and business impact`,
    {
      input: reviews,
      primaryKey: 'id',
      output: { name: 'analysis', schema: SentimentSchema }
    }
  );

  if (!result.success) {
    console.error('Analysis failed:', result.error);
    return;
  }

  // Rows may come back in any order, so look each one up by its primary key
  const reviewsById = new Map(reviews.map(r => [r.id, r]));

  // Group by urgency
  const urgent = result.data.filter(r => r.analysis.responseUrgency === 'high');
  const actionable = result.data.filter(r => r.analysis.isActionable);
  const positive = result.data.filter(r => r.analysis.sentiment === 'positive');
  const negative = result.data.filter(r => r.analysis.sentiment === 'negative');

  // Summary report
  console.log('Customer Feedback Analysis');
  console.log('='.repeat(50));
  console.log(`Total reviews: ${reviews.length}`);
  console.log(`Positive: ${positive.length} | Neutral: ${result.data.length - positive.length - negative.length} | Negative: ${negative.length}`);
  console.log(`Urgent responses needed: ${urgent.length}`);
  console.log(`Actionable feedback: ${actionable.length}`);

  // Urgent items
  if (urgent.length > 0) {
    console.log('\n--- URGENT: Requires Immediate Response ---\n');
    for (const row of urgent) {
      const review = reviewsById.get(row.id)!;
      console.log(`[${row.id}] ${review.source}`);
      console.log(`  "${review.text.slice(0, 80)}..."`);
      console.log(`  Action: ${row.analysis.actionRequired}`);
      console.log();
    }
  }

  // Actionable items
  console.log('\n--- All Actionable Feedback ---\n');
  for (const row of actionable) {
    const review = reviewsById.get(row.id)!;
    const a = row.analysis;
    console.log(`[${row.id}] ${a.sentiment} (${a.score.toFixed(2)}) - Urgency: ${a.responseUrgency}`);
    console.log(`  Topics: ${a.topics.join(', ')}`);
    console.log(`  Action: ${a.actionRequired}`);
    console.log();
  }

  // Stats
  console.log('='.repeat(50));
  console.log(`Tokens: ${result.usage.totalTokens} | Latency: ${result.latencyMs}ms`);
}

analyzeReviews();
```

## Output

From a run on Claude Code, with the default logger's `[INFO]` lines left out. The wording of free-text fields such as the next step varies from run to run:

```text
Customer Feedback Analysis
==================================================
Total reviews: 5
Positive: 2 | Neutral: 1 | Negative: 2
Urgent responses needed: 2
Actionable feedback: 2

--- URGENT: Requires Immediate Response ---

[rev-002] support-ticket
  "App crashes every time I try to export. Very frustrating. Been waiting 3 days fo..."
  Action: Investigate and fix the crash on export; respond to the customer's support ticket right away, since they have waited 3 days.

[rev-004] twitter
  "The new update broke my workflow. I rely on this for my business. Please fix ASA..."
  Action: Reply publicly on Twitter, find out which workflow the update broke, and prioritize a fix or rollback for this business-critical user.

--- All Actionable Feedback ---

[rev-002] negative (-0.85) - Urgency: high
  Topics: app crash, export feature, support response time
  Action: Investigate and fix the crash on export; respond to the customer's support ticket right away, since they have waited 3 days.

[rev-004] negative (-0.80) - Urgency: high
  Topics: update regression, workflow disruption, business reliance
  Action: Reply publicly on Twitter, find out which workflow the update broke, and prioritize a fix or rollback for this business-critical user.

==================================================
Tokens: 3104 | Latency: 6125ms
```

## Processing Daily Batches

When you analyze reviews every day, keep the prompt string constant and pass each day's reviews as `input`. Nuabase puts the prompt and schema at the start of the request, so the provider can serve that part from its prompt cache:

```typescript
const result = await nua.list(ANALYZE_REVIEWS_PROMPT, {
  input: todaysReviews,
  primaryKey: 'id',
  output: { name: 'analysis', schema: SentimentSchema },
});

if (result.success) {
  const { promptTokens, promptCacheReadTokens } = result.usage;
  if (promptCacheReadTokens !== undefined) {
    console.log(`${promptCacheReadTokens} of ${promptTokens} prompt tokens read from the provider's cache`);
  }
}
```

The cache fields are present only when the provider reports them. For very large batches, split the reviews into chunks; see [Large arrays](/docs/api/list.md#large-arrays).

## Variations

### Simple Sentiment Only

For basic sentiment without detailed analysis:

```typescript
const SimpleSentimentSchema = z.object({
  sentiment: z.enum(['positive', 'neutral', 'negative']),
  confidence: z.number().min(0).max(1),
});

const result = await nua.list("Classify sentiment", {
  input: reviews,
  primaryKey: 'id',
  output: { name: 'sentiment', schema: SimpleSentimentSchema }
});
```

### Multi-Aspect Sentiment

Analyze different aspects separately:

```typescript
const AspectSentimentSchema = z.object({
  overall: z.enum(['positive', 'neutral', 'negative']),
  aspects: z.object({
    product: z.enum(['positive', 'neutral', 'negative', 'not-mentioned']),
    support: z.enum(['positive', 'neutral', 'negative', 'not-mentioned']),
    pricing: z.enum(['positive', 'neutral', 'negative', 'not-mentioned']),
    usability: z.enum(['positive', 'neutral', 'negative', 'not-mentioned']),
  }),
});
```

### Competitor Mentions

Track competitor mentions in feedback:

```typescript
const CompetitorSchema = z.object({
  sentiment: z.enum(['positive', 'neutral', 'negative']),
  mentionsCompetitor: z.boolean(),
  competitors: z.array(z.string()).optional(),
  comparisonSentiment: z.enum(['favorable', 'unfavorable', 'neutral']).optional(),
});
```
