# AI for Every Row: Bulk LLM to Classify & Summarize Datasets (`humble-echidna/ai-rows`) Actor

Run an LLM (GPT, Gemini and more) on every row of a dataset or JSON list, for AI enrichment and sentiment analysis: classify, tag, score, summarize or extract fields, chained after any scraper with datasetId. Answers validated against your schema or labels; $2 per 1,000 valid rows plus model tokens.

- **URL**: https://apify.com/humble-echidna/ai-rows.md
- **Developed by:** [Michael Costa](https://apify.com/humble-echidna) (community)
- **Categories:** AI, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.40 / 1,000 row processeds

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

### What does AI for Every Row do?

**AI for Every Row** runs an LLM on **every row of a dataset**: classify, tag, score, summarize or pull fields out
of each one. Give it **any Apify dataset** (chain it after a review, lead, job or web scraper) or a JSON list, say
which fields hold the text, and what you want back: **labels to choose from**, plain-English fields, or a **JSON
Schema**. Every answer is **validated**, and you pay only for rows that match. Typical jobs: **AI enrichment** of
leads or listings, and **sentiment analysis** of reviews.

It is **not** a scraper: it works on rows you already have. To read web pages, use
[AI Data Extractor](https://apify.com/humble-echidna/ai-extract) instead ([which one?](#ai-for-every-row-or-ai-data-extractor)).

[Input fields](https://apify.com/humble-echidna/ai-rows/input-schema) ·
[API](https://apify.com/humble-echidna/ai-rows/api) ·
[Use it from Claude, ChatGPT or Cursor (MCP)](#can-i-use-ai-for-every-row-from-an-ai-agent-mcp)

**Jump to:** [Fields](#what-data-does-ai-for-every-row-return) ·
[Price](#how-much-does-it-cost-to-run-an-llm-on-every-row) ·
[How to use](#how-to-classify-or-summarize-every-row-of-a-dataset) ·
[Chain after a scraper](#chain-it-after-any-scraper) ·
[Only new rows](#only-new-rows-for-scheduled-pipelines) ·
[Input](#input) · [Output](#output) ·
[AI agents (MCP)](#can-i-use-ai-for-every-row-from-an-ai-agent-mcp) · [Limits](#limits) · [FAQ](#faq)

**Try it in one click:** the input comes pre-filled with three short product reviews we made up, and the labels
positive, negative and mixed. That's 3 rows: $0.006, plus the $0.00005 start fee, plus well under $0.001 of model
tokens on a paid Apify plan. **Then replace them with your own rows or dataset.**

### What data does AI for Every Row return?

One result per input row, in the order they finish, including the rows that failed (with the reason, never charged).

| Field | Example | Notes |
|---|---|---|
| `data` | `{"label": "negative"}` | The answer: `{label}` with labels, your fields as camelCase keys, or your schema's shape. |
| `valid` | `true` | `true` when `data` matches the schema. Only these rows are charged. |
| `errors` | `["label: 'furious' is not one of ['positive', 'negative', 'mixed']"]` | Why a row has no valid answer: no text, a failed call, or what didn't match. Empty when valid. |
| `sourceIndex` | `1` | The row's position in your input (0-based). |
| `kept` | `{"id": "r2"}` | The `keepFields` copied from the row, to join results back; `null` when none are set. |
| `model` | `openai/gpt-4.1-mini` | The model that read the row. |
| `inputTokens`, `outputTokens` | `283`, `5` | Tokens the model reported: what the token charge is based on. |
| `textChars`, `truncated` | `98`, `false` | Characters of row text sent; `true` if it was cut to fit `maxTokensPerRow`. |
| `processedAt` | `2026-10-02T14:03:11Z` | When the run read the row. |

The full list is under [Output](#output).

### How much does it cost to run an LLM on every row?

Two parts, both on your Apify account:

1. **This actor: $2.00 per 1,000 rows processed** ($0.002 a row), plus $0.00005 each time a run starts. Only rows
   whose answer matched your schema or labels count.
2. **The model's tokens**, billed by Apify's own [OpenRouter actor](https://apify.com/apify/openrouter), which this
   actor calls with your run's token. On paid Apify plans that's OpenRouter's list price; on the Free plan the
   OpenRouter actor charges 10 times as much (its pricing, not ours). A short review with a label took about 220
   input and 6 output tokens (measured on 200 rows, 2026-10-02): with the default `openai/gpt-4.1-mini`, **about
   $0.10 per 1,000 rows** on a paid plan.
   Longer texts cost more; `maxTokensPerRow` caps it, and `openai/gpt-4.1-nano` costs about a quarter as much.

- **The example below:** 3 rows × $0.002 = $0.006, plus the start fee and under $0.001 of tokens.
- **A month, for example:** 20,000 new reviews a month from a scheduled review scraper, each labelled and summarized
  in one sentence with **Only new rows** on: 20,000 × $0.002 = $40, plus about $4-6 of tokens (paid plan).
- **Caps:** **Max rows per run** in the input, and **Maximum cost per run** in the run options (it covers this
  actor's charges; the token charges are the OpenRouter actor's, so cap those with **Max rows per run** and
  **Max tokens of text per row**). Each row is counted against the limit before it's sent, and given back if it
  fails, so a capped run never sends a row it can't return.

**Never charged by this actor:** rows with no text in your text fields, failed model calls, answers that don't
match your schema or labels, answers with nothing in them (every value `null`, blank or a stand-in like "N/A"), and
rows skipped by **Only new rows**. (A row that reached the model still used tokens.)

### How to classify or summarize every row of a dataset

1. Open AI for Every Row and click **Try for free** (or **Start** if you're signed in).
2. Pick your rows: a dataset in **Or: rows from a dataset**, or paste a JSON array into **Rows**.
3. In **Text fields**, name the fields that hold the text, e.g. `text`, `title`, `description` (dotted paths such as
   `review.body` work). Only these go to the model.
4. Say what you want back: **Labels to choose from** (`positive`, `negative`, `mixed`), or **fields to get** in plain
   English (`summary: one sentence, topics, language`), or a **JSON Schema**. Add **Instructions** for the task
   ("Classify each company's industry from its description").
5. Name the **Fields to copy to the output** you'll join on (`id`, `url`), click **Start**, then open the **Output**
   tab and export as JSON, CSV or Excel.

#### Labels, plain-English fields or a JSON Schema?

- **Labels** are for classification: the model must pick exactly one, returned as `data.label`; anything else comes
  back `valid: false`, uncharged. They apply only when fields and schema are empty.
- **Fields** are quickest for everything else: each becomes a camelCase key (`main complaint` → `mainComplaint`)
  holding text, a number, true/false, a list of texts or `null`. Add a hint after a colon: `score: 1 to 5`.
- **A JSON Schema** gives exact types, enums and nesting, e.g. a label *and* a score:
  `{"type": "object", "properties": {"label": {"enum": ["bug", "feature", "question"]}, "urgency": {"type": "integer", "minimum": 1, "maximum": 5}}, "required": ["label", "urgency"]}`.

### Chain it after any scraper

Point it at another actor's results and each item is one row. Only the top-level fields named in **Text fields**
and **Fields to copy** are read from the dataset, so big items (a place with all its reviews) cost little to read.

- **Once:** pick the dataset in **Or: rows from a dataset** (`datasetId`) and click **Start**.
- **Every time the other actor finishes:** on that actor (or its task), open **Integrations**, add **Run an actor
  or task**, pick AI for Every Row, and set the input to include `"datasetId": "{{resource.defaultDatasetId}}"` plus
  your `textFields` and labels, fields or schema.

Good pairs from our own actors (any other actor's dataset works the same way):

| First run | Then label or summarize | Text fields |
|---|---|---|
| [Website & Page to Markdown](https://apify.com/humble-echidna/page-to-markdown) | Each page's topic, audience or a summary | `markdown` |
| [AI Data Extractor](https://apify.com/humble-echidna/ai-extract) | Classify what it extracted (e.g. a company's services into your industry list) | `data` |
| [Product Price & Stock Monitor](https://apify.com/humble-echidna/product-offers) | Each product's category in your own taxonomy | `name`, `brand` |
| [Hiring Signals](https://apify.com/humble-echidna/hiring-signals) | Which teams each company is hiring for, from its new roles | `company`, `newRoles` |

Have a CSV or Excel file instead? Turn it into a dataset first with
[Dataset Transformer](https://apify.com/humble-echidna/dataset-transform), then chain this after it.

### Only new rows, for scheduled pipelines

Turn on **Only new rows** (`"onlyNew": true`) and each run skips rows whose text the same setup already answered
validly in an earlier run, so a daily run after a scraper only pays for what's new. A run with nothing new costs only
the start fee.

- **What counts as the same setup:** the labels, fields or schema, the instructions, the model and the text fields.
  Change any of them and every row is new to it. The dataset doesn't matter: the same text gets the same answer.
- **What's remembered:** a hash of each row's text (not the text), in a key-value store named `ai-rows-seen` in your
  own account, for the last 100,000 rows per setup. Rows that failed or didn't validate aren't remembered, so the
  next run tries them again. Two identical texts in one run are sent once.
- **Big datasets:** a run that nears its timeout stops starting rows, saves what it finished and ends cleanly; the
  next run with **Only new rows** carries on where it stopped.

### Example: sentiment of three reviews

The pre-filled input (three reviews we wrote; `product` is never sent, only `text`). On Apify on 2026-10-02 the
default input labelled all three as intended (positive, negative, mixed) in 9 seconds:

```json
{
  "rows": [
    {"id": "r1", "product": "Steel kettle", "text": "Boils fast and the handle stays cool. Exactly what I wanted for the price."},
    {"id": "r2", "product": "Steel kettle", "text": "Started leaking from the base after two weeks, and support never answered my emails."},
    {"id": "r3", "product": "Desk lamp", "text": "Lovely warm light and easy to assemble, but the switch feels flimsy."}
  ],
  "textFields": ["text"],
  "labels": ["positive", "negative", "mixed"],
  "keepFields": ["id"]
}
```

What the model is sent for the second row, all of it besides the instructions and the schema:
`text: Started leaking from the base after two weeks, and support never answered my emails.` One result:

```json
{
  "sourceIndex": 1,
  "kept": {"id": "r2"},
  "data": {"label": "negative"},
  "valid": true,
  "errors": [],
  "model": "openai/gpt-4.1-mini",
  "textChars": 90,
  "inputTokens": 218,
  "outputTokens": 6,
  "truncated": false,
  "processedAt": "2026-10-02T14:24:41Z"
}
```

### Input

| Field | What it does |
|---|---|
| **Rows** (`rows`) | A JSON array of objects (a plain string is `{"text": ...}`). Ignored when a dataset is set. |
| Or: rows from a dataset (`datasetId`) | Another actor's results; chain with `{{resource.defaultDatasetId}}`. |
| **Text fields** (`textFields`) | The fields the model reads, default `text`. The only fields sent. |
| Labels to choose from (`labels`) | Classification: one label per row, as `data.label`. Default positive, negative, mixed. |
| Or: fields to get (`fields`) | Plain-English fields. Overrides labels. |
| Or: JSON Schema for one record (`schema`) | A JSON Schema with `"type": "object"` at the top. Overrides fields and labels. |
| Instructions (`instructions`) | The task in your words, up to 4,000 characters. |
| Model (`model`) | Any OpenRouter model id with structured outputs. Default `openai/gpt-4.1-mini`. |
| Fields to copy to the output (`keepFields`) | Copied into `kept` for joins; not sent to the model. |
| Only new rows (`onlyNew`) | Skip rows this setup already answered. Default off. |
| Max rows per run (`maxRows`) | Send at most this many rows (1-20,000); empty = all. |
| Max tokens of text per row (`maxTokensPerRow`) | Cap on each row's text, default 4,000 (100-100,000). |

```json
{
  "datasetId": "<your dataset id>",
  "textFields": ["description"],
  "instructions": "Classify each company by its main industry, from its description.",
  "labels": ["Software", "Finance", "Healthcare", "Retail", "Manufacturing", "Other"],
  "keepFields": ["name", "website"],
  "model": "openai/gpt-4.1-mini",
  "maxRows": 500
}
```

**Which model?** The default, `openai/gpt-4.1-mini`, is accurate and cheap for labels and short fields. For big
batches of short rows, `openai/gpt-4.1-nano` or `google/gemini-2.5-flash-lite` cost about a quarter as much; for
subtle judgments or long texts, a larger model. The model must support structured outputs.

### Output

One result per row:

```json
{
  "sourceIndex": 4,
  "kept": {"id": "r5"},
  "data": {"label": "furious"},
  "valid": false,
  "errors": ["label: 'furious' is not one of ['positive', 'negative', 'mixed']"],
  "model": "openai/gpt-4.1-mini",
  "textChars": 41,
  "inputTokens": 214,
  "outputTokens": 6,
  "truncated": false,
  "processedAt": "2026-10-02T14:03:11Z"
}
```

A row with no text in your text fields has `data: null`, `textChars: 0` and the reason in `errors`, and isn't sent.
The run's `RUN_STATS` record counts rows read, processed, invalid, empty, failed, without text, and skipped by
**Only new rows** (done before, or a repeat in the run), tokens used, and the options that applied. The **Tokens** view of the dataset shows
tokens per row.

### Run it on a schedule, or from your own code

1. Save your input as a [task](https://docs.apify.com/platform/actors/running/tasks) (**Save as a new task**, top right
   of the actor page) and add it to a [schedule](https://docs.apify.com/platform/schedules) (Console → Schedules →
   **Create new**), or chain it after a scheduled scraper ([how](#chain-it-after-any-scraper)).
2. Collect results: download the dataset as JSON, CSV or Excel; fetch the latest run's results from the
   [API](https://apify.com/humble-echidna/ai-rows/api)
   (`GET https://api.apify.com/v2/actor-tasks/<task id>/runs/last/dataset/items?status=SUCCEEDED&format=csv`, with
   your API token); let a [webhook](https://docs.apify.com/platform/integrations/webhooks) tell your system when a
   run succeeds; or connect it to Make, Zapier or n8n through
   [Apify's integrations](https://docs.apify.com/platform/integrations).

#### Can I use AI for Every Row from an AI agent (MCP)?

Yes, through [Apify's MCP server](https://docs.apify.com/platform/integrations/mcp), from Claude, ChatGPT, Cursor or
any other MCP client. Add this to your client's MCP configuration (or let the agent find it with the server's actor
search); your client signs you in to Apify:

```json
{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com?tools=humble-echidna/ai-rows"
    }
  }
}
```

To use an [Apify API token](https://console.apify.com/settings/integrations) instead of signing in, add
`"headers": {"Authorization": "Bearer <APIFY_TOKEN>"}` next to `url`.

An agent can label a few rows itself; this pays off for a whole dataset: pass
`{"datasetId": "...", "textFields": ["text"], "labels": ["bug", "feature", "question"], "maxRows": 1000}` and get one
validated answer per row without spending the agent's own context on the rows.

### Who it's for

Anyone with a scheduled scraper (reviews, leads, jobs, listings, articles) who wants each row labelled, scored or
summarized on its way to a spreadsheet or CRM: review sentiment and complaint themes, lead industry and size,
job seniority, ticket triage, article summaries.

### Why this one?

- **Takes any Apify dataset.** Chain it after any actor with `datasetId`, no export and re-upload; or pass rows
  inline. It reads only the fields it needs.
- **Validated, not just generated.** Every answer is checked against your labels or schema here, whatever the model
  promised. One that doesn't match is marked `valid: false` with the reasons, and isn't charged.
- **Only new rows.** A scheduled run pays only for rows it hasn't answered before.
- **Only the text you pick goes to the model.** Names, emails and every other field stay out of the request.
- **$2 per 1,000 rows, tokens at the model's price on paid plans.** Pick any OpenRouter model; `inputTokens` and
  `outputTokens` show the token cost per row.

### Limits

- **One call per row, one answer per row.** For an answer about many rows together (a summary of all reviews),
  this isn't the tool.
- **Up to 20,000 rows per run.** For bigger datasets, schedule it with **Only new rows**; each run takes the next
  rows.
- **Speed:** 20 rows in flight at once; the model's answer time sets the pace. The same 200 short reviews took
  22 seconds in one run and 106 in another on Apify (2026-10-02, `openai/gpt-4.1-nano`), so plan on 2-10 rows a
  second. A run that nears its timeout (1 hour by default) stops
  cleanly and says so; with **Only new rows** on, the next run continues.
- **Your text fields are checked first.** If none of the first 100 rows has text in them, the run stops before
  sending anything and lists the fields the first row has.
- **Long texts are cut** to `maxTokensPerRow` (the start is kept; `truncated: true`). Raise it for long texts.
- **Answers are capped at 2,048 tokens** by Apify's OpenRouter proxy. A longer answer comes back `valid: false`
  with the reason, uncharged: ask for fewer fields or shorter values.
- **Models can still be wrong**, even with a valid answer. Validation checks the shape, not the judgment;
  spot-check a sample before you rely on a new setup.
- **No files.** It reads datasets and inline rows; for a CSV, Excel or JSON file, load it with
  [Dataset Transformer](https://apify.com/humble-echidna/dataset-transform) first.

### FAQ

#### AI for Every Row or AI Data Extractor?

**Extract from web pages → [AI Data Extractor](https://apify.com/humble-echidna/ai-extract)**: give it page URLs (or
a dataset of URLs) and it fetches each page and extracts fields. **Already have rows → AI for Every Row**: it never
fetches anything, and works on the text already in your rows (reviews, descriptions, Markdown from
[Website & Page to Markdown](https://apify.com/humble-echidna/page-to-markdown)).

#### What is sent to the model?

Only the values of your text fields, each as `field: value`, plus your instructions and the schema. Every other
field of the row, including names, emails and ids, stays in your account. Fields you copy with `keepFields` go to
the output, not to the model.

#### Can I use it on reviews or other text about people?

Labelling each review or message (sentiment, topic, urgency) is what it's for. Don't use it to profile people: to
score, rank or infer things about the individuals who wrote the text (their character, health, politics or the
like). Leave author names and ids out of the text fields; keep them with `keepFields` only if you need to join, and
only if you have a lawful basis to hold them.

#### Who pays for the model?

You do, through your own Apify account: the actor calls Apify's OpenRouter actor with your run's token, and Apify
bills the tokens to the account running the actor. No API key is needed and none of ours is used.

#### Why did a row come back `valid: false`?

The `errors` field says why: the row had no text in your text fields, the model call failed, the answer wasn't a
label from your list, or it didn't match your schema. None of these are charged. Run it again with **Only new
rows** on and only those rows are retried.

### Related actors

| Actor | Use it when |
|---|---|
| [AI Data Extractor](https://apify.com/humble-echidna/ai-extract) | You have web page URLs, not rows: it fetches each page and extracts fields. |
| [Website & Page to Markdown](https://apify.com/humble-echidna/page-to-markdown) | You want page text to label or summarize here. |
| [Dataset Transformer](https://apify.com/humble-echidna/dataset-transform) | You need to load a CSV/Excel file, or filter and reshape the results afterwards. |

### Feedback and support

Found a bug, or a task it handles badly? Open an issue on the **Issues** tab with the input you used.

### Versions

Current version: **0.1**. See the Changelog tab for what changed in each version.

# Changelog

This Actor's version history is a separate document: https://apify.com/humble-echidna/ai-rows/changelog.md

# Actor input Schema

## `rows` (type: `array`):

Your rows as a JSON array of objects, e.g. \[{"id": "r1", "text": "Boils fast, handle stays cool."}]; a plain string is the row {"text": ...}. The default is three made-up product reviews. Ignored when datasetId is set. At most 20,000 rows per run.

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

Default empty. One of your Apify datasets, picked here or given by id, e.g. a review, lead or job scraper's results; to chain this actor after another in an integration, `{{resource.defaultDatasetId}}`. Each item is one row (rows is ignored). Reads at most 20,000 items per run, read-only, only the fields named in textFields and keepFields.

## `textFields` (type: `array`):

The row fields whose values the model reads, e.g. `text`, `title`, `description`; dotted paths like `review.body` work. Only these leave the row: names, emails and every other field are never sent. Each goes as "field: value". Default `text`. At most 20.

## `labels` (type: `array`):

Classification: the model picks exactly one of these for each row, returned as data.label, e.g. positive, negative, mixed (the default). Used only when fields and schema are empty. At most 200 labels; for a label plus other fields, use schema.

## `fields` (type: `string`):

Default empty. What to get from each row, comma-separated or one per line, e.g. `summary, topics, language`. Add a hint after a colon: `summary: one sentence, under 20 words`. Each becomes a camelCase key in data holding text, a number, true/false, a list of texts or null. Overrides labels; ignored when schema is set. At most 50 fields.

## `schema` (type: `object`):

Default empty. A JSON Schema for the object to return per row, with "type": "object" at the top, e.g. {"type": "object", "properties": {"label": {"enum": \["bug", "feature", "question"]}, "score": {"type": "number"}}}. Every answer is validated against it; one that doesn't match is returned with valid false and errors, and not charged. Overrides fields and labels.

## `instructions` (type: `string`):

Default empty. The task in your words, applied to every row, e.g. `Classify the industry of each company from its description` or `Summarize each article for a newsletter`. At most 4,000 characters.

## `model` (type: `string`):

The model that reads each row, as an OpenRouter model id. Default `openai/gpt-4.1-mini`. Cheaper for big batches of short rows: `openai/gpt-4.1-nano` or `google/gemini-2.5-flash-lite`. It must support structured outputs. Its tokens are billed to your Apify account by Apify's OpenRouter actor, at its prices.

## `keepFields` (type: `array`):

Default empty. Row fields copied unchanged into each result's kept object, so you can join results back, e.g. `id`, `url`, `placeId`. They are not sent to the model (unless also in textFields). Every result also has sourceIndex, the row's position. At most 20.

## `onlyNew` (type: `boolean`):

Default false. When true, rows whose text this same setup (schema or fields or labels, instructions, model, textFields) already answered validly in an earlier run are skipped and not charged, so a scheduled run after a scraper only pays for new rows. Remembered per text hash in your account.

## `maxRows` (type: `integer`):

Send at most this many rows to the model, in input order (with onlyNew, the first new rows), e.g. 100 for a test. From 1 to 20000; leave empty (the default) for every row. The run also stops cleanly at the maximum cost per run you set in the run options.

## `maxTokensPerRow` (type: `integer`):

Hard cap on each row's text sent to the model, in tokens (about 4 characters each). Default 4000. A longer text keeps its start and has truncated true. From 100 to 100000. Lower it to cut token cost on long texts.

## Actor input object example

```json
{
  "rows": [
    {
      "id": "r1",
      "product": "Steel kettle",
      "text": "Boils fast and the handle stays cool. Exactly what I wanted for the price."
    },
    {
      "id": "r2",
      "product": "Steel kettle",
      "text": "Started leaking from the base after two weeks, and support never answered my emails."
    },
    {
      "id": "r3",
      "product": "Desk lamp",
      "text": "Lovely warm light and easy to assemble, but the switch feels flimsy."
    }
  ],
  "textFields": [
    "text"
  ],
  "labels": [
    "positive",
    "negative",
    "mixed"
  ],
  "model": "openai/gpt-4.1-mini",
  "keepFields": [
    "id"
  ],
  "onlyNew": false,
  "maxTokensPerRow": 4000
}
```

# Actor output Schema

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

No description

## `runStats` (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 = {
    "rows": [
        {
            "id": "r1",
            "product": "Steel kettle",
            "text": "Boils fast and the handle stays cool. Exactly what I wanted for the price."
        },
        {
            "id": "r2",
            "product": "Steel kettle",
            "text": "Started leaking from the base after two weeks, and support never answered my emails."
        },
        {
            "id": "r3",
            "product": "Desk lamp",
            "text": "Lovely warm light and easy to assemble, but the switch feels flimsy."
        }
    ],
    "textFields": [
        "text"
    ],
    "labels": [
        "positive",
        "negative",
        "mixed"
    ],
    "model": "openai/gpt-4.1-mini",
    "keepFields": [
        "id"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("humble-echidna/ai-rows").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 = {
    "rows": [
        {
            "id": "r1",
            "product": "Steel kettle",
            "text": "Boils fast and the handle stays cool. Exactly what I wanted for the price.",
        },
        {
            "id": "r2",
            "product": "Steel kettle",
            "text": "Started leaking from the base after two weeks, and support never answered my emails.",
        },
        {
            "id": "r3",
            "product": "Desk lamp",
            "text": "Lovely warm light and easy to assemble, but the switch feels flimsy.",
        },
    ],
    "textFields": ["text"],
    "labels": [
        "positive",
        "negative",
        "mixed",
    ],
    "model": "openai/gpt-4.1-mini",
    "keepFields": ["id"],
}

# Run the Actor and wait for it to finish
run = client.actor("humble-echidna/ai-rows").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 '{
  "rows": [
    {
      "id": "r1",
      "product": "Steel kettle",
      "text": "Boils fast and the handle stays cool. Exactly what I wanted for the price."
    },
    {
      "id": "r2",
      "product": "Steel kettle",
      "text": "Started leaking from the base after two weeks, and support never answered my emails."
    },
    {
      "id": "r3",
      "product": "Desk lamp",
      "text": "Lovely warm light and easy to assemble, but the switch feels flimsy."
    }
  ],
  "textFields": [
    "text"
  ],
  "labels": [
    "positive",
    "negative",
    "mixed"
  ],
  "model": "openai/gpt-4.1-mini",
  "keepFields": [
    "id"
  ]
}' |
apify call humble-echidna/ai-rows --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,humble-echidna/ai-rows"
        }
    }
}
```

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/2djECEAhS4nem0Z4F/builds/o4LDQeubekbT04cD1/openapi.json
