# Review Analyzer: Sentiment, Complaints, Themes & Reply Drafts (`nerolabs/review-analyzer`) Actor

Returns sentiment, complaint and praise themes, verbatim quotes, urgent flags, a written report per business and optional reply drafts for reviews from any Google Maps, Trustpilot, Yelp or Amazon dataset, CSV or Google Sheet. Charged per review, report and reply. Agent-ready: x402, MCP.

- **URL**: https://apify.com/nerolabs/review-analyzer.md
- **Developed by:** [Adam Pearce](https://apify.com/nerolabs) (community)
- **Categories:** AI, Business, Automation
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.40 / 1,000 review analyzeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Review Analyzer: Sentiment, Complaints, Themes & Reply Drafts

**Scraped 2,000 Google Maps, Trustpilot or Yelp reviews and need to know what customers actually complain about, what they love, and whether it is getting better or worse?** Point this Actor at the scraper's dataset (or a CSV or Google Sheet) and you get back:

- **Every review, analysed**, next to your original columns: sentiment (positive, mixed, neutral, negative), complaint and praise **categories** from one fixed list (staff attitude, waiting times, cleanliness, price and value, quality of work and 15 more) plus short **themes** in the customer's own terms ("rude reception staff", "left mess behind"), a **key quote copied word for word**, a one-line summary and an **urgent flag** for safety risks, legal threats and customers still waiting for something they paid for.
- **One report per business**: average stars, star breakdown, negative share, owner reply rate, the **top complaints and top praise with real counts and verbatim quotes**, **what is changing** (last 90 days against before), and **suggested fixes** tied to each complaint. Also saved as a readable HTML page you can send to a client.
- **Optional reply drafts**, ready to post, for every review the owner has not answered yet. Replies respond to the customer's actual points and **never promise refunds, discounts or compensation, never admit fault and never invent facts**: every draft is checked in code and a draft that breaks a rule is rewritten once, then withheld and not charged.

**Nothing is invented.** Every quote is checked against the review text and dropped if it is not there word for word. Every count, average, share and trend is computed from your data, never by the AI; the AI only writes the words around them.

It reads the output of any reviews scraper without setup. Review text, star rating (out of 5, 10 or 100), date, business and owner-reply columns are detected automatically, including nested ones such as `rating.value` and `dates.publishedDate`. Tested on real data from the Google Maps Reviews Scraper (including Tripadvisor-sourced rows that carry `rating: "4/5"` instead of `stars`) and from a Trustpilot reviews scraper (review headline, `replyMessage` owner replies, `companyName` and `companyDomain`), in 9 languages.

### Who uses it

- **Agencies and reputation managers**: a monthly "what your customers are saying" report per client location, plus reply drafts to approve, from one scraper run.
- **Multi-location businesses** (hotels, restaurants, gyms, clinics, trades): compare branches, find the one where complaints about waiting times are rising.
- **Local service businesses**: see your top three complaints in plain words and what to fix first.
- **Product and e-commerce teams**: tag Amazon, Trustpilot or app store reviews by issue and track it over time.
- **Lead generation**: find businesses with many unanswered negative reviews, a ready-made reason to get in touch.
- **AI agents**: a reviews dataset in, a structured analysis and a written report out, in one call.

### How to use it

1. Run a reviews scraper on Apify (for example the Google Maps Reviews Scraper, or any Trustpilot, Yelp, Amazon, TripAdvisor or Booking.com reviews scraper), then pick its dataset in **Dataset**. Or paste a CSV, Excel, JSON or Google Sheet link into **File or Google Sheet URL**, or paste reviews into **Review texts** with a **Business name**.
2. Optional: switch on **Write reply drafts** and add your sign-off and a contact line for unhappy customers. Add **Your own themes** if you want every point tagged from your own list.
3. Run. 250 reviews take about 1 to 2 minutes. Review rows stream into the dataset as they are ready; business report rows come at the end (`rowType: "business_report"`, or open the **Business reports** view), the readable report is saved as `report.html` and a run summary as `OUTPUT`.

Chain it after a scraper with an Apify integration or webhook ("run this Actor when the scraper finishes, with its dataset ID"), and schedule it monthly with **Also append to named dataset** to build a history.

### Sample output

One review from the default example (public Google reviews of a large London hotel, from a real cloud run with reply drafts on, sign-off "The team" and a contact line), with most original columns left out and the review text shortened:

```json
{
  "title": "Premier Inn London County Hall hotel",
  "stars": 3,
  "text": "Great location for this hotel but that's where it ends. ... I arrived between 4.30 and 5pm and was told the room was not ready as the cleaners hadn't cleaned it yet. ...",
  "rowType": "review",
  "reviewBusiness": "Premier Inn London County Hall hotel",
  "reviewStars": 3,
  "reviewDate": "2026-01-25T21:22:11.400Z",
  "analysisStatus": "ok",
  "sentiment": "negative",
  "complaintCategories": [
    "speed_and_waiting",
    "customer_service",
    "price_and_value",
    "rooms_and_facilities"
  ],
  "complaintThemes": [
    "room not ready at check-in time",
    "no apology for delay",
    "expensive room for quality",
    "uncomfortable bed causing sore back"
  ],
  "praiseCategories": [
    "location_and_access"
  ],
  "praiseThemes": [
    "great location"
  ],
  "keyQuote": "Great location for this hotel but that's where it ends",
  "reviewSummary": "Customer appreciated location but complained about late room readiness, no apology, expensive price, and uncomfortable bed.",
  "needsAttention": false,
  "ownerReplied": false,
  "replyDraft": "We are sorry to hear about your experience. Thank you for highlighting our great location. We understand the frustration caused by the room readiness and bed comfort and appreciate your feedback. Please call us so we can look into this.\nThe team",
  "replyStatus": "drafted"
}
```

And part of the business report row for the same hotel (250 reviews). Theme counts are reviews per group, so one review can count in two groups:

```json
{
  "rowType": "business_report",
  "reviewBusiness": "Premier Inn London County Hall hotel",
  "avgStars": 4.44,
  "reviewsTotal": 250,
  "negativeShare": 0.06,
  "ownerReplyRate": 0.44,
  "overview": "Guests generally rate the hotel highly and most reviews are positive. Reviewers repeatedly praise the central location and friendly staff while some raise practical room and service issues.",
  "topComplaints": [
    {
      "category": "atmosphere_and_noise",
      "headline": "Street and motorbike noise disturbs sleep",
      "whatCustomersSay": "Multiple guests report late-night motorcycles, car racing and street noise that made sleep difficult or impossible. Reports describe noise continuing late into the night and waking children.",
      "mentions": 9,
      "shareOfReviews": 0.05,
      "recentDirection": "steady",
      "themes": [
        {
          "theme": "motor vehicle noise at night",
          "mentions": 6
        },
        {
          "theme": "street gatherings and racing",
          "mentions": 5
        }
      ],
      "exampleQuote": "we were kept up from 11pm to 4 am by NONSTOP cars and motorcycles racing outside, burning out, revving engines"
    }
  ],
  "trend": {
    "direction": "stable",
    "explanation": "Last 90 days: 88 reviews, average 4.48 stars, 6% negative. Before that: 162 reviews, average 4.42 stars, 7% negative."
  },
  "suggestedFixes": [
    {
      "fix": "Inspect and service all AC units; record fixes and offer portable fans when needed.",
      "addresses": "rooms_and_facilities"
    }
  ]
}
```

### Accuracy

Hand-checked on 04-10-2026 against real Google reviews of two UK electricians and a large London hotel (in English, French, German, Danish, Polish, Portuguese, Korean, Japanese and more):

- **Sentiment, categories and themes right on 28 of 30 randomly picked reviews.** The two misses were small: "nice hotel" read as neutral instead of positive, and a value-for-money point filed under the wrong category. Four AI models were tested on the same reviews; the one used here tied for the best accuracy and wrote the most careful replies.
- **Quotes: every quote shown is in the review word for word.** About 1 in 20 suggested quotes is dropped by the check for not being exact.
- **Reply drafts** thanked customers for their specific points, invited unhappy ones to get in touch, and passed the refund, discount, fault and legal checks.

It is an AI reading text, so spot-check a sample before acting on it in bulk.

### Pricing

Pay per event, no subscription:

| Event | Price |
|---|---|
| Review analyzed (one review with sentiment, categories, themes, quote, summary and urgency flag) | $0.002 ($2 per 1,000 reviews), less on Bronze, Silver and Gold plans |
| Business report (written report for a business with at least 5 reviews with text) | $0.03 per business |
| Reply drafted (only if switched on) | $0.005 per reply that passes the checks |
| CSV or Excel export file | $0.01 each, only if requested |
| Webhook delivery | $0.02, only on a 2xx response |

**Examples:** 500 Google reviews of one restaurant with a report costs about $1.03. Adding reply drafts for the 200 the owner never answered adds $1.00. An agency's monthly run over 20 client locations with 50 new reviews each costs about $2.60 with reports. Star-only reviews with no words are counted in the report but never sent to the AI and never charged; failed AI calls and withheld replies are never charged. Set the run's maximum charge to cap spend; the Actor stops cleanly at the limit and charges nothing it did not deliver.

### Input

| Field | What it does |
|---|---|
| `datasetId` | Apify dataset of reviews (any scraper's output) |
| `fileUrl` | CSV, TSV, Excel, JSON or JSON Lines link, or a Google Sheet shared as "anyone with the link" |
| `reviewTexts` + `businessName` | Reviews pasted as plain text, reported under one business name |
| `data` | Rows as inline JSON |
| `output` | `reviews_and_reports` (default), `reviews_only` or `reports_only` |
| `minReviewsForReport` | Minimum reviews with text for a written report, default 5 |
| `themes` | Your own theme list; leave empty for automatic themes |
| `replyDrafts`, `replyOnlyUnanswered`, `replyTone`, `replySignOff`, `replyContact`, `replyLanguage` | Reply drafts; off by default |
| `textField`, `ratingField`, `dateField`, `businessField` | Only if auto-detection picks the wrong column; nested fields use a dot (`review.body`) |
| `keepOriginalFields`, `exportFormats`, `outputDatasetName`, `webhookUrl`, `concurrency`, `maxItems` | Output and run options |

### FAQ

**Will a reply draft ever offer a refund or admit we were at fault?** No. Replies are written under rules that forbid refunds, money back, compensation, discounts, vouchers, freebies, admissions of fault or liability, legal talk and accusing the reviewer, and every draft is then checked in code. A draft that fails is rewritten once; if it still fails it is withheld (`replyStatus: "withheld_failed_safety_check"`) and not charged. Your sign-off and contact line are added exactly as you wrote them. Always read a reply before posting it.

**Which scrapers does it work with?** Any that output a review text or rating column: Google Maps reviews, Trustpilot, Yelp, Amazon, TripAdvisor, Booking.com, Airbnb, G2, Capterra, app store reviews and survey exports. The run log names the columns it used. Reviews are grouped into one report per place, company, product or app.

**How is "what is changing" worked out?** In code: the reviews from the last 90 days (counted back from the newest review) are compared with all the earlier ones on average stars and negative share. It needs at least 5 dated reviews on each side, and a single category's rising or falling arrow needs at least 10, so a small business with a handful of reviews is told honestly that there is not enough data yet.

**What about reviews in other languages?** Reviews in most languages are read; themes and summaries come back in English, and replies are written in the review's own language unless you set **Reply language**.

**Is my data stored?** Only the review text, stars and business name are sent to the AI model (OpenAI) for the run; nothing is kept afterwards.

**Is it agent-ready?** Yes. Pay per event, works through Apify's MCP server and x402 payments, with typed fields, fixed category names, and a summary record an agent can read.

If this saved you reading reviews by hand, a short review on the Apify Store helps a lot and is read personally. Questions, or a scraper whose columns are not picked up? Open an issue on the Issues tab and it will be answered the same day.

# Actor input Schema

## `datasetId` (type: `string`):

An Apify dataset of reviews, for example the output of a Google Maps Reviews Scraper, Trustpilot, Yelp, Amazon, TripAdvisor, Booking.com or app store reviews scraper run. Every original column is kept and the analysis is added alongside. Review text, star rating, date, business and owner reply columns are found automatically. Use the picker rather than typing an ID.

## `fileUrl` (type: `string`):

A public link to a CSV, TSV, Excel, JSON or JSON Lines file with one review per row (a review platform export, a survey export, a sheet of scraped reviews). A normal Google Sheets link works: share it as 'Anyone with the link can view'.

## `fileFormat` (type: `string`):

Leave on 'Detect automatically' unless the link has no file extension and the server reports the wrong content type.

## `sheetName` (type: `string`):

Which sheet to read from an Excel workbook. Defaults to the first sheet.

## `reviewTexts` (type: `array`):

A plain list of reviews pasted as text, one per entry, for a quick run with no file. Use a dataset, file or Google Sheet to keep star ratings, dates and your own columns.

## `data` (type: `array`):

Review rows as inline JSON, an alternative to a dataset or file. Each object needs a review text or rating field.

## `businessName` (type: `string`):

Name to use when your data has no business column (for example pasted review texts or one company's export). Reviews are grouped into one report per business; with no business column and no name they are reported together as 'All reviews'.

## `output` (type: `string`):

What to return. 'Reviews and business reports' gives one analysed row per review plus one report row per business (rowType tells them apart). 'Reviews only' skips the reports. 'Business reports only' returns just the report rows; every review is still analysed and charged, because the report is built from them.

## `minReviewsForReport` (type: `integer`):

A written business report (charged per business) is made only for businesses with at least this many reviews with text. Below it, the report row still shows the counts and averages, free.

## `themes` (type: `array`):

Leave empty and the AI names each complaint and praise point in a few plain words. Or give your own list (for example 'waiting time', 'staff', 'cleanliness', 'price') and every point is tagged with one of yours, so runs stay comparable month to month.

## `replyDrafts` (type: `boolean`):

Adds a ready-to-post owner reply to each review (charged per reply, on top of the review). Replies thank the customer, respond to their actual points, and never promise refunds, discounts or compensation, never admit fault and never invent facts. A draft that breaks a rule is rewritten once, then withheld and not charged.

## `replyOnlyUnanswered` (type: `boolean`):

Skip reviews that already have an owner reply (when the data has an owner reply column). Turn off to draft a reply for every review.

## `replyTone` (type: `string`):

How the replies should sound.

## `replySignOff` (type: `string`):

Added at the end of every reply, for example 'The Riverside Cafe team' or 'Sam, Owner'. Leave empty for no sign-off.

## `replyContact` (type: `string`):

Optional line added, exactly as written, to replies to negative reviews and to mixed reviews of 3 stars or less, for example 'Please call us on 01234 567890 so we can put this right.' Without it, replies to complaints just invite the customer to get in touch, and never invent a phone number or email.

## `replyLanguage` (type: `string`):

Leave as 'same' to reply in each review's own language, or name one language (for example 'English' or 'Spanish') for every reply.

## `textField` (type: `string`):

Override the detected review text column (nested fields use a dot, e.g. 'review.body'). Leave empty to detect automatically.

## `ratingField` (type: `string`):

Override the detected rating column. Ratings out of 5, 10 or 100 are all read as stars out of 5.

## `dateField` (type: `string`):

Override the detected review date column. Used for the 'what is changing' trend.

## `businessField` (type: `string`):

Override the detected business column (place name, company, product or app). One report is written per value.

## `keepOriginalFields` (type: `boolean`):

Keep every input column next to the analysis. Turn off to return only the review text and the analysis.

## `exportFormats` (type: `array`):

Also save the analysed reviews as a CSV and/or Excel file in the run's key-value store (charged per file). The readable HTML report is always saved free when reports are written.

## `outputDatasetName` (type: `string`):

Optional name of a dataset to append every output row to, so monthly runs build one history.

## `webhookUrl` (type: `string`):

Optional URL that receives the run summary and the business reports as JSON when the run finishes (charged per successful delivery). Works with Zapier, Make, n8n and Slack workflows.

## `concurrency` (type: `integer`):

How many batches of 20 reviews are sent to the AI at once. The default suits almost every run.

## `maxItems` (type: `integer`):

Stop after this many rows. Handy for a cheap first test on a big dataset.

## Actor input object example

```json
{
  "fileUrl": "https://nerolabs-samples.nerolabs.workers.dev/sample-reviews.json",
  "fileFormat": "auto",
  "output": "reviews_and_reports",
  "minReviewsForReport": 5,
  "replyDrafts": false,
  "replyOnlyUnanswered": true,
  "replyTone": "warm",
  "replyLanguage": "same",
  "keepOriginalFields": true,
  "concurrency": 4
}
```

# Actor output Schema

## `results` (type: `string`):

Every original review row with sentiment, complaint and praise themes, a verbatim key quote, an urgency flag and optional reply draft, plus one report row per business.

## `report` (type: `string`):

The business reports as one page you can open in a browser or send to a client.

## `summary` (type: `string`):

Fields used, reviews analysed, sentiment counts, reply and report counts, export links and warnings.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "fileUrl": "https://nerolabs-samples.nerolabs.workers.dev/sample-reviews.json"
};

// Run the Actor and wait for it to finish
const run = await client.actor("nerolabs/review-analyzer").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "fileUrl": "https://nerolabs-samples.nerolabs.workers.dev/sample-reviews.json" }

# Run the Actor and wait for it to finish
run = client.actor("nerolabs/review-analyzer").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "fileUrl": "https://nerolabs-samples.nerolabs.workers.dev/sample-reviews.json"
}' |
apify call nerolabs/review-analyzer --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,nerolabs/review-analyzer"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/g4MGdowyjcOQI90Oj/builds/QdlbjNLjJa56XsXMA/openapi.json
