# Sentiment Analysis - Reviews, Reddit & Comments to Insights (`ntriqpro/sentiment-analysis-product-insights`) Actor

Sentiment analysis of reviews, Reddit posts and comments you already have: per text sentiment, product issue, product names and exact sentences, plus AI issue analysis and issue clusters (on by default). 1,000 texts: about $17.60 on BRONZE, $1.60 rules only. Reads Actor datasets and CSV.

- **URL**: https://apify.com/ntriqpro/sentiment-analysis-product-insights.md
- **Developed by:** [daehwan kim](https://apify.com/ntriqpro) (community)
- **Categories:** AI, Business, Social media
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.40 / 1,000 ai insight extracteds

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

## Sentiment Analysis - Reviews, Reddit & Comments to Insights

Turn the reviews, Reddit posts, comments and support tickets you already have into product insights. Each text comes back as one row: its **sentiment** (positive, negative, mixed or neutral), the **product issue** it raises (complaint, request, question, praise or usage context), the **product or feature** it names and the **exact sentence** that shows it — in English and Korean. On paid plans an **AI analysis** reads every text for its product issues, each with a quote from the text, and **issue clusters** count how many texts raise the same problem of the same product (battery life, overheating and charging trouble are separate clusters).

Feed it the results of another Apify Actor directly (Reddit, app reviews, Google Maps reviews, Threads, Telegram, Naver blog, YouTube comments…), a CSV or JSON export, or texts you paste.

**What a default run costs:** 1,000 texts with the AI analysis (on by default) ≈ **$17.60** on the BRONZE plan ($0.004 run start + 1,000 × $0.0016 rules + 1,000 × $0.016 AI). With the rules only (`aiInsights: false`): **$1.60**. Issue clusters add $0.04 a cluster row (default up to 50: at most **$19.60** in all). The run-start fee includes reading the first 1,000 rows of your sources; each further 1,000 rows read is $0.0016 (`rows-read`). Your run's maximum charge is always respected.

### Free plan and paid plans

| | Free plan (preview) | Paid Apify plan (Starter and up) |
|---|---|---|
| Texts per run | the first 1 | up to `maxTexts` (default 1,000, 20,000 a run) |
| Sentiment and one sentence that shows it | on that one | on every text |
| The rest of the rule verdict (categories, your product names, every evidence sentence with its category, score and signal words) | locked — the row says how many a paid run gives for that text (`categories.count`, `targets.count`, `evidenceLocked.count`), never the values | on every text |
| AI analysis (beta): product issues, product or feature, one-line summary, exact quote | locked — a free run asks no model | on by default, for up to `aiMaxTexts` texts (5,000 a run) |
| Issue clusters (3+ texts about the same problem of the same product: count, share, sentiment, 1–3 quotes, links) | the upper bound a paid run of your settings could make (`maxClusters`; the free plan does not analyse your other texts, so it gives no count) | one row per cluster, up to `maxClusters` (200 a run) |
| What a paid run of the same input gets | in the `free-plan-preview` row: how many texts your input holds (a free run reads up to 5,000 rows), how many are locked, the estimate of **this run** with your settings and of **the whole input** (how many runs, the `startAt` of each, the total; BRONZE prices). No analysis of the other texts — no rows, no sentiment or category counts | — |
| Price | `run-started` and `text-scored` | all five events, with plan discounts |

#### Rules and AI on the same text (fixed example)

The rules part below is what this Actor's rules return for these two made-up texts. **The AI part is the model's real answer for them** (`claude-haiku-5-5`, 2026-10-16, after our checks). A free run shows only the sentiment and one sentence of your first text; this is the whole output.

**English** — `Love the Pixel 8 camera, but the battery dies by 3pm and it gets hot while charging. Please add a battery saver mode.`

- rules: mixed; complaint, request, praise — the battery and the heat share one complaint sentence (and "gets hot" is not on the word list), and that same sentence is also the praise.
- AI: mixed; praise · `Pixel 8` · "Love the Pixel 8 camera" — complaint · `battery` · "the battery dies by 3pm" — complaint · `battery` · "it gets hot while charging" (three issues at most: the request was the fourth).

**Korean** — `갤럭시 S24 화면은 정말 선명한데 배터리가 반나절도 못 가요. 업데이트 후로 카메라 앱이 자꾸 튕겨요. 절전 모드 좀 개선해 주세요.`

- rules: mixed; complaint, request, praise — "배터리가 반나절도 못 가요" has no word on the list, so the battery complaint is missed and its sentence counts as praise ("선명").
- AI: mixed; praise · `화면` · "화면은 정말 선명한데" — complaint · `배터리` · "배터리가 반나절도 못 가요" — complaint · `카메라 앱` · "업데이트 후로 카메라 앱이 자꾸 튕겨요".

The free preview row carries the same example in its `example` field.

Paid-plan discounts (applied by Apify to every event of this Actor):

| Apify plan | Discount |
|---|---|
| FREE | list price |
| BRONZE (Starter) | 20% off |
| SILVER (Scale) | 35% off |
| GOLD and up (Business, Enterprise) | 48% off |

### What it is for

- **Product teams**: what users complain about and ask for, by feature, with the sentences to paste into a planning doc.
- **Customer support**: sort a ticket export into complaints, questions and requests; find the product that causes most of them.
- **Marketing and research**: sentiment of Reddit threads, app reviews or comments about your product or a competitor's, with source links.
- **Korean feedback**: Naver blog posts, Coupang or app reviews in Korean are read with a Korean word list, and by the AI.

### How to use it

1. Run a scraper you already use (for example a Reddit, Google Play or Google Maps reviews scraper) and copy its dataset ID, or upload a CSV export somewhere public, or paste texts.
2. Put the dataset ID in `datasetIds`, the file address in `csvUrls` or the texts in `texts`. Add your product and feature names to `productHints`.
3. Run it. Open the **Texts** view for one row per text, **Issue clusters** for the grouped issues, and the last rows for the notices and the run report (`limits`). On the free plan, open **Free plan preview and paid estimate** for what a paid run of the same input would analyse and cost.

### Input

| Field | Type | Description |
|---|---|---|
| `datasetIds` | array | IDs of datasets in your Apify account, read with this run's token (the run-start fee pays for the first 1,000 rows of all sources; each further 1,000 rows read is a `rows-read` event). Text fields are found by themselves: `text`, `body`, `content`, `reviewText`, `reviewBody`, `review`, `feedback`, `comment`, `commentText`, `selftext`, `caption`, `postText`, `fullText`, `message`, `snippet`, `description` (letter case, spaces and `_` aside). A post with a title and a body (a Reddit post) is analysed as "title, blank line, body". Notice and summary rows of other Actors are left out |
| `csvUrls` | array | Public http(s) addresses of CSV (header row; comma, semicolon, tab or pipe; a file with one column is read by the same rules: a quoted cell may run over several lines, and reading stops at its first row that is not valid CSV), JSON or JSONL files, up to 20 MB each (their rows count as rows read; a CSV row with more cells than the header row is left out, not charged; at a row that is not valid CSV — a quote never closed, or text right after a closing quote — reading of that file stops: the rows before it are read, from its line on nothing is read or charged; both come with a `source-parse-error` notice; empty cells at the end of a row are ignored and a shorter row gets empty cells) |
| `texts` | array | Texts pasted here, one per entry (or objects `{ "text", "url", "id", "date" }`); they count as rows read too |
| `textField` | string | The text field's name, when it has another one (tried first) |
| `productHints` | array | Your product and feature names, up to 30 |
| `language` | string | `auto` (default), `en` or `ko` — which word lists the rules use |
| `aiInsights` | boolean | AI analysis on paid plans of the whole text a row holds, up to 5,000 characters (default `true`; 1,000 texts ≈ $17.60 on BRONZE with it, $1.60 without) |
| `aiMaxTexts` | integer | Texts analysed by AI in a run, 1–5,000 (default 1,000) |
| `clusters` | boolean | Issue cluster rows on paid plans (default `true`; $0.04 a row on BRONZE) |
| `maxClusters` | integer | Cluster rows in a run, 1–200 (default 50) |
| `maxTexts` | integer | Texts in a run, 1–20,000 (default 1,000; free plan: 1 — the free preview uses this value for its estimates) |
| `maxItemsPerSource` | integer | Rows read from one dataset or file, 1–100,000 (default 20,000) |
| `startAt` | integer | To go on from an earlier run of the same input: texts before this place (in input order, each once) are passed over, not analysed or charged as texts. The earlier run's `input-limit` row, status and `limits.resume.nextStartAt` give the exact value. Passed-over rows are still read (rows read). Default 0 |

A row is one review: rows with **different IDs** are different texts even when their words are the same (each is a row and a `text-scored` event; the AI analyses those words once and charges once — see `ai.sameTextAs`). The same ID with the same words given twice, or the same words without an ID (letter case and spacing aside), is one text. Two texts that differ only past their 5,000th character are two texts. Rows without a text field and texts shorter than 3 characters are skipped and no text row is charged for them; they count as rows read, and a notice row says how many.

**Large inputs.** A run takes up to `maxTexts` texts. When your sources hold more, the run's `input-limit` row and status give `nextStartAt`: run the same input again with `startAt` set to it, and the next run takes the next texts without analysing or charging the earlier ones again. Each run reads the sources from the start (rows before `startAt` count as rows read).

### Output

A text row (`recordType: "text"`):

| Field | What it holds |
|---|---|
| `text`, `title`, `truncated` | The text as analysed — by the rules and by the AI (up to 5,000 characters; `truncated: true` when the source text was longer), the title it came with |
| `sourceType`, `sourceRef`, `sourceId`, `sourceUrl`, `publishedAt`, `rating` | Where it came from: `dataset`, `file` or `input`, which one, the row's id, address, date and star rating (1–5) when the source has them |
| `language` | `en`, `ko` or `other` |
| `sentiment`, `sentimentSource` | `positive`, `negative`, `mixed` or `neutral`; `ai` when the AI analysis was delivered, else `rules` |
| `categories`, `primaryCategory` | Rule categories: `complaint`, `request`, `question`, `praise`, `usage-context` |
| `targets` | Your `productHints` the text names |
| `evidence[]` | For each category, the sentence that shows it: `quote` (cut out of `text`, letter for letter), `start`, `end`, `category` |
| `rules` | The rule verdict: `sentiment`, `score` (−1 to 1 from the counts of signal words — **not a probability**), `signals` (the words that counted, `not …` when a negation turned one around) |
| `ai` | `status`: `analysed` with `sentiment`, `noProductIssue` (about the `analyzedChars` characters it read — the whole `text`), `issues[]` (`category`, `target`, `issue`, `quote`, `start`, `end`), `analyzedChars`, `model`, `modelRequests`, and `sameTextAs` (the `textKey` of the row whose analysis this is, when an earlier row had the same words: not charged again); or `locked` (free plan, with `paidGets`), `off`, or `not-analysed` with a `reason` |
| `textKey`, `bodyKey` | Fixed keys (16 characters of a SHA-256): `textKey` of the row (its ID and words), also used in `issue-cluster.textKeys`; `bodyKey` of its words alone |
| Free plan | `categories`, `primaryCategory`, `targets`, `evidenceLocked`, `rules` are `{ "locked": true, "count", "paidGets" }`: what a paid run gives for this text, as a count; `evidence[]` holds one sentence without its category |
| `plan`, `locked[]`, `billedEvents[]`, `analyzedAt`, `disclaimer`, `rowId` | The plan, what is withheld, the events this row is charged for, when, the notice below, and the row's fixed id within the run |

An issue cluster row (`recordType: "issue-cluster"`, paid plans) is one problem: the same `category`, the same product name (`target`; `null` with `productNamed: false` for texts that name none of your products — a cluster of their own) and the same `topic` (`battery-life`, `overheating`, `charging`, `crashes`, `connectivity`, `performance`, `display`, `sound`, `camera`, `build`, `price`, `shipping`, `support`, `software` or `other`, from the words of each issue's quote). Fields: `label` (for example `complaint: Pixel 8 — battery life`), `topicLabel`, `texts` (how many reviews — rows with the same words count each), `distinctTexts` (how many different wordings), `share` (of the run's texts), `sentimentMix`, `languages`, `quotes[]` (1 to 3 representative texts' quotes, each with `sourceUrl` and `textKey`), `sourceUrls[]` (up to 20), `textKeys[]`, `firstSeen`, `lastSeen`, `basis` (`ai`, `rules` or `mixed`).

Example text row (paid plan, AI analysis on; the rules part is what the rules give, the AI part is the model's answer measured on 2026-10-16):

```json
{
  "recordType": "text",
  "text": "Love the Pixel 8 camera, but the battery dies by 3pm and it gets hot while charging. Please add a battery saver mode.",
  "sentiment": "mixed",
  "sentimentSource": "ai",
  "categories": ["complaint", "request", "praise"],
  "primaryCategory": "complaint",
  "targets": ["Pixel 8"],
  "evidence": [
    { "quote": "Love the Pixel 8 camera, but the battery dies by 3pm and it gets hot while charging.", "start": 0, "end": 84, "category": "complaint" },
    { "quote": "Please add a battery saver mode.", "start": 85, "end": 117, "category": "request" },
    { "quote": "Love the Pixel 8 camera, but the battery dies by 3pm and it gets hot while charging.", "start": 0, "end": 84, "category": "praise" }
  ],
  "rules": { "sentiment": "mixed", "score": 0, "signals": { "positive": ["love"], "negative": ["dies"] } },
  "ai": {
    "status": "analysed",
    "sentiment": "mixed",
    "noProductIssue": false,
    "issues": [
      { "category": "praise", "target": "Pixel 8", "issue": "The writer likes the camera of the Pixel 8.", "quote": "Love the Pixel 8 camera", "start": 0, "end": 23 },
      { "category": "complaint", "target": "battery", "issue": "The battery runs out by mid-afternoon.", "quote": "the battery dies by 3pm", "start": 29, "end": 52 },
      { "category": "complaint", "target": "battery", "issue": "The device gets hot while charging.", "quote": "it gets hot while charging", "start": 57, "end": 83 }
    ],
    "analyzedChars": 117,
    "model": "claude-haiku-5-5",
    "modelRequests": 1
  },
  "plan": "paid",
  "billedEvents": ["text-scored", "insight-extracted"]
}
```

The same text on the free plan: `"sentiment": "mixed"`, `"evidence": [{ "quote": "Love the Pixel 8 camera, but the battery dies by 3pm and it gets hot while charging.", "start": 0, "end": 84 }]`, and `"categories": { "locked": true, "count": 3, … }`, `"targets": { "locked": true, "count": 1, … }`, `"evidenceLocked": { "locked": true, "count": 2, … }`, `"rules": { "locked": true, … }`, `"ai": { "status": "locked", … }`.

Notice rows (`noticeCode`, `message`) are never charged: `free-plan-preview`, `source-unreadable`, `source-no-text`, `source-parse-error` (rows of a CSV file with more cells than the header row were left out: `skippedRows`, `firstSkippedLine`; `restNotRead: true` and `notReadFromLine` when a row that is not valid CSV starts on that line: from there the file was not read — not tried, not found invalid, and not charged; the rows before it were read; that source's `readCut` is `"csv-errors"`), `rows-skipped`, `duplicate-input`, `invalid-input`, `input-limit` (with `nextStartAt`: the `startAt` of the next run), `reading-stopped` (the maximum charge did not pay for reading the next 1,000 rows: the texts read before are analysed as usual), `ai-off`, `ai-stopped-charge-limit` (the run's maximum charge stopped paying for AI analyses: the remaining texts go on with the rule verdict and no AI charge), `charge-limit-reached`, `billing-unavailable` (after a restart the platform's charge counts did not show earlier charges yet: nothing charged again, resurrect the run later) and others. The last row of a run, `recordType: "limits"`, is its report: what each source gave, how many rows were read and what the reading was charged (`reading`), where the next run goes on (`resume.nextStartAt`; `resume.inputFinished` is `true` when nothing is left, `false` when texts are left and `null` when it is not known: the run had `maxTexts` texts and whether more follow is not known, or a CSV file stopped at a row that is not valid CSV — its rest was not read, and `startAt` does not reach it: fix the row and run again; the status message says which file and line), what was analysed, what was charged.

### AI analysis (optional, beta, paid plans)

- **When it is on, the texts are sent to the Anthropic API (United States)** with e-mail addresses and phone numbers replaced by `[email]` and `[phone]`. The model is `claude-haiku-5-5`. Turn it off with `aiInsights: false`.
- The texts and your product names are sent as data, not instructions: a text that tells the model what to answer is not obeyed.
- **Every answer is checked before it is delivered or charged**: each quote must be in your text letter for letter, each product or feature must be named in the text, and the summary may not write a number the text does not hold or any web or e-mail address. An answer that fails one check is thrown away whole and not charged; the row keeps its rule verdict and says why (`ai.reason`).
- An answer that the text raises no product issue (`noProductIssue: true`) is an analysis too and is charged.
- It reads the product, never the person: no scores of anyone's personality, politics, health or mood are made.
- **What it reads:** the whole text the row holds (up to 5,000 characters; `ai.analyzedChars`). "No product issue" is about all of it.
- **Measured before release** (2026-10-16, 30 real public texts: 15 Korean App Store reviews and 15 English Reddit comments): 30 of 30 answers passed our checks, none was refused; our model cost was about $0.0004 per analysis (about $0.0013 for a Korean text of 5,000 characters). Texts with the same words in one run are analysed once.

### Pricing

Pay per event. Your run's maximum charge is always respected: rows are counted against it before they are written, and a run that reaches it ends normally (SUCCEEDED) with a notice.

| Event | FREE | BRONZE | SILVER | GOLD and up | When |
|---|---|---|---|---|---|
| `insight-extracted` | $0.02 | $0.016 | $0.013 | $0.0104 | An AI analysis that passed our checks is delivered on a text row (paid plans) |
| `text-scored` | $0.002 | $0.0016 | $0.0013 | $0.00104 | A text is delivered as a row with its rule verdict — also when it is neutral |
| `issue-cluster` | $0.05 | $0.04 | $0.0325 | $0.026 | A cluster row of 3 or more texts (paid plans) |
| `run-started` | $0.005 | $0.004 | $0.00325 | $0.0026 | Once per run that has something to read, before the sources are read — also when they turn out to hold no usable text. Includes reading the first 1,000 rows |
| `rows-read` | $0.002 | $0.0016 | $0.0013 | $0.00104 | Paid plans: each further 1,000 rows (or part) actually read from your sources, all together — rows without a text, duplicates and rows passed over with `startAt` too. Asked of your maximum charge before the rows are read |

`insight-extracted` is the AI analysis that is on by default. Not charged: notices, rows without a text, duplicates, AI answers we threw away, and an input with nothing to read (no source and no text of 3 characters or more: no `run-started` either). Reading: the first 1,000 rows of a run are in `run-started`; each further 1,000 rows read is a `rows-read` event (a source that cannot be opened is not read and not charged). A restarted run reads again, without a new event, the rows it has already paid for. A free run reads at most 5,000 rows, without `rows-read`. Platform usage is included.

Example: 1,000 app reviews on the BRONZE plan with AI analysis (the default) and 20 clusters ≈ $0.004 + 1,000 × $0.0016 + 1,000 × $0.016 + 20 × $0.04 = **$18.40**; the same with the rules only (`aiInsights: false`, `clusters: false`) = **$1.60**. 2,000 reviews with the default `maxTexts` (1,000) take two runs (`startAt` 0 and 1,000): about **$35.21** with AI and without clusters — the second run reads 2,000 rows, one `rows-read` event.

### Limits and good to know

- The rule verdict uses word lists and patterns. It is fast and free of model calls, and it misses sarcasm and context — that is what the AI analysis is for. Its `score` is not a probability.
- Two runs started at the same time on the same input each deliver and charge their own rows.
- A run that is restarted by the platform goes on where it stopped: rows already in the dataset are not written or charged again.
- The platform's charge counts can show a charge a few minutes late. When a restarted run's counts still do not show what it charged before the restart, it does not charge those events again and stops (`billing-unavailable`): resurrect the run a few minutes later and it goes on from its own dataset. A new run, even with the same input, starts over and charges the texts again.
- A run reads at most what its maximum charge pays for: when the limit does not pay for the next 1,000 rows it stops reading and delivers what it read, with the `startAt` to go on from.

### Responsibility

This is an unofficial, independent tool. It is not affiliated with, endorsed by or sponsored by Reddit, Google, Apple, Meta, Telegram, Naver or any site your texts come from. It analyses only the texts you supply and sends no messages. **You are responsible** for having the right to process these texts, including any personal data in them (for example under GDPR, CCPA or PIPA), and for complying with the terms of the sites they come from. You are the data controller for the output. Scores and categories are automated estimates, not facts about any person.

# Actor input Schema

## `datasetIds` (type: `array`):

IDs of datasets in your Apify account (for example the results of a Reddit, app review or comment scraper run). They are read with this run's own token; the run-start fee pays for reading the first 1,000 rows of all sources together, each further 1,000 rows read is a rows-read event (paid plans). Common text fields are found by themselves (text, body, content, reviewText, review, comment, selftext, caption, message…); notice and summary rows of other Actors are left out. A Reddit post with a title and a body is analysed as one text.

## `csvUrls` (type: `array`):

Public http(s) addresses of files with one text per row (a support-ticket export, a review export…). CSV needs a header row; headers are matched without letter case, spaces or underscores ("Review Text" = reviewText). Up to 20 MB a file; its rows count as rows read (the first 1,000 rows of all sources are in the run-start fee). Addresses inside private networks are refused.

## `texts` (type: `array`):

Texts pasted here, one per entry. In JSON you can also give objects: { "text": "…", "url": "…", "id": "…", "date": "…" }. Rows with different ids are different texts even when their words are the same; the AI analyses one wording once.

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

The field that holds the text, when it has a name we do not find by ourselves. Tried before the common names.

## `productHints` (type: `array`):

Names you want texts sorted by (for example "battery", "app", "camera", "충전기"). A text that names one gets it in `targets`; the AI and the clusters use the names the text itself writes. Up to 30, each up to 60 characters.

## `language` (type: `string`):

"auto" reads each text with the English and the Korean word lists; "en" or "ko" uses one list only. The AI reads any language.

## `aiInsights` (type: `boolean`):

For each text: up to three product issues, each with its category, the product or feature it is about, a one-line summary and the exact passage of the text it rests on. The AI reads the whole text the row holds (up to 5,000 characters). On by default. Charged per checked analysis (insight-extracted, $0.016 on BRONZE): 1,000 texts with it on cost about $17.60 on BRONZE (run start + rules + AI), $1.60 with it off. Answers that fail our checks are thrown away and not charged. When on, the texts are sent to the Anthropic API (United States) with e-mail addresses and phone numbers removed. Free plan: locked (no model is asked).

## `aiMaxTexts` (type: `integer`):

Paid plans. The first this many texts of the run get an AI analysis; the rest keep the rule verdict.

## `clusters` (type: `boolean`):

On by default. After the texts: one row for each problem that 3 or more texts raise - the same category, the same product name (or none named) and the same issue topic (battery life, overheating, charging, crashes…) - with how many, their share, their sentiment, 1 to 3 quotes and source links. Charged per cluster row (issue-cluster, $0.04 on BRONZE; the default 50 rows at most add $2.00). Free plan: no cluster is made; the preview row gives the bound a paid run can reach (maxClusters), not a count of your texts.

## `maxClusters` (type: `integer`):

Paid plans. The largest clusters first.

## `maxTexts` (type: `integer`):

Paid plans: up to 20,000 (default 1,000). Free plan: 1, whatever is set here; the free preview uses this value to estimate this run and how many runs the whole input takes. Texts are counted after duplicates are removed; every run stays within its maximum charge. When the input holds more, the run's last rows give the startAt of the next run.

## `maxItemsPerSource` (type: `integer`):

The most rows read from each dataset or text file. A source with more rows is cut here; the run's summary row says so for that source (`readCut: "maxItemsPerSource"`). Rows read past the first 1,000 of all sources together are charged as `rows-read` (paid plans, per 1,000 or part).

## `startAt` (type: `integer`):

To go on from an earlier run of the same input: the texts before this place (in input order, each once, counted as the run counts them) are passed over - not analysed or charged as texts. The earlier run's input-limit row and status give the exact value (nextStartAt). The passed-over rows are still read and count as rows read. 0: from the first text.

## Actor input object example

```json
{
  "texts": [
    "The battery dies after two hours and the app keeps crashing since the last update. Please add a battery saver mode!",
    "Love the camera, the night photos are great. I use it every day on my commute.",
    "배송은 빨랐는데 충전기가 고장났어요. 환불은 어떻게 하나요?"
  ],
  "productHints": [
    "battery",
    "app",
    "camera",
    "충전기"
  ],
  "language": "auto",
  "aiInsights": true,
  "aiMaxTexts": 1000,
  "clusters": true,
  "maxClusters": 50,
  "maxTexts": 1000,
  "maxItemsPerSource": 20000,
  "startAt": 0
}
```

# Actor output Schema

## `dataset` (type: `string`):

No description

# 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 = {
    "texts": [
        "The battery dies after two hours and the app keeps crashing since the last update. Please add a battery saver mode!",
        "Love the camera, the night photos are great. I use it every day on my commute.",
        "배송은 빨랐는데 충전기가 고장났어요. 환불은 어떻게 하나요?"
    ],
    "productHints": [
        "battery",
        "app",
        "camera",
        "충전기"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("ntriqpro/sentiment-analysis-product-insights").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 = {
    "texts": [
        "The battery dies after two hours and the app keeps crashing since the last update. Please add a battery saver mode!",
        "Love the camera, the night photos are great. I use it every day on my commute.",
        "배송은 빨랐는데 충전기가 고장났어요. 환불은 어떻게 하나요?",
    ],
    "productHints": [
        "battery",
        "app",
        "camera",
        "충전기",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("ntriqpro/sentiment-analysis-product-insights").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 '{
  "texts": [
    "The battery dies after two hours and the app keeps crashing since the last update. Please add a battery saver mode!",
    "Love the camera, the night photos are great. I use it every day on my commute.",
    "배송은 빨랐는데 충전기가 고장났어요. 환불은 어떻게 하나요?"
  ],
  "productHints": [
    "battery",
    "app",
    "camera",
    "충전기"
  ]
}' |
apify call ntriqpro/sentiment-analysis-product-insights --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,ntriqpro/sentiment-analysis-product-insights"
        }
    }
}
```

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/j0gxbJtkzbwqb9YQ1/builds/wdBt1ZA71AsLHVnc3/openapi.json
