# Bulk AI Prompt: ChatGPT-style LLM on Every Row (`olliolli/bulk-ai-prompt`) Actor

Run one AI prompt on every row of a CSV, Excel file, Google Sheet or dataset: classify, extract fields, summarise, write lines. Answers as new columns. All-inclusive price, no API key.

- **URL**: https://apify.com/olliolli/bulk-ai-prompt.md
- **Developed by:** [Tom J](https://apify.com/olliolli) (community)
- **Security:** Limited permissions
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 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.

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

Run **one AI prompt on every row** of a CSV, Excel file, Google Sheet or dataset: classify reviews, extract fields, sort products into categories, summarise tickets or write a first line per lead. It works like running ChatGPT-style prompts in bulk, and the answers come back as **new columns** next to your data. Model included: **no API key**, no OpenAI account. **$2 per 1,000 rows** with the standard model.

### What you can do with it

- **Classify text in bulk**: sentiment of 5,000 reviews, topic of support tickets, lead quality.
- **Extract structured fields**: job title, city and "remote yes/no" from job ads, as separate columns.
- **Categorise products**: map titles and descriptions to your own category list.
- **Write personalised lines**: one opening sentence per company for outreach.
- **Summarise**: one-sentence summary of every ticket, note or review.

### How to run AI on every row of a CSV or Google Sheet in 3 steps

1. **Add your data**: a link to a CSV, Excel file or Google Sheet, a dataset from another Actor, or JSON rows.
2. **Write your prompt** with column names in double curly braces, e.g. `Classify the sentiment of this review: {{review}}`. Optionally list output fields such as `sentiment`.
3. **Click Start.** You get one result row per input row, in the same order, with your columns plus `ai_answer` or `ai_<field>`, also saved as `results.csv`.

### Ready-made examples

| Task | Prompt | Output fields |
|---|---|---|
| Sentiment | `Classify the sentiment of this review as positive, neutral or negative: {{review}}` | `sentiment` |
| Product category | `Pick the best category (Home, Garden, Kitchen, Electronics, Other). Title: {{title}}. Description: {{description}}` | `category`, `confidence` |
| Field extraction | `From this job ad, extract the job title, the city and whether it is remote. Ad: {{text}}` | `job_title`, `city`, `remote` |
| Outreach line | `Write one friendly opening sentence for an email to {{company}}, a {{industry}} company in {{city}}. No greeting, no sign-off.` | — |
| Summary | `Summarise this support ticket in one sentence: {{body}}` | — |

Tip: temperature 0 for classifying and extracting; about 0.7 for writing.

### Your data

| Source | Field | Notes |
|---|---|---|
| CSV / TSV / Excel / Google Sheet link | `fileUrl` | First sheet, first row = column names; Google Sheets shared "Anyone with the link" (the tab in the link is used); Drive, Dropbox, OneDrive links work; max 50 MB |
| Another Actor's dataset | `datasetId` | Items become rows |
| JSON rows | `rows` | `[{"review": "Arrived fast, works well"}]` |

One source per run: dataset first, then file link, then JSON rows. CSV separators (comma, semicolon, tab, pipe) and encodings (UTF-8, UTF-16, Latin-1) are detected.

### Placeholders

- `{{column}}` inserts a value; names are not case-sensitive and spaces inside braces are ignored.
- `{{address.city}}` and `{{tags.0}}` reach nested values; objects and lists are inserted as compact JSON.
- Without any placeholder, the whole row is added as JSON.
- A row whose placeholders are all empty is skipped (`skipped_empty`) and not charged.

### Input

| Field | Meaning | Default |
|---|---|---|
| `promptTemplate` | Instruction per row (required) | — |
| `outputFields` | Up to 20 fields, each becomes column `ai_<field>` | plain answer in `ai_answer` |
| `systemPrompt` | Role, language, tone for every row | short default |
| `model` | `standard` or `large` | `standard` |
| `maxRows` | Rows read | 100 (max 10,000) |
| `maxInputChars` | Prompt length per row | 6,000 |
| `maxOutputTokens` | Answer length per row | 400 |
| `temperature` | 0 = predictable, higher = varied | 0.2 |
| `includeOriginalColumns` | Keep your columns in the output | on |

### Output

```json
{
  "row_number": 1,
  "input_preview": "Bright, easy to set up and it arrived a day early…",
  "product": "Desk lamp",
  "review": "Bright, easy to set up and it arrived a day early. Very happy with it.",
  "ai_sentiment": "positive",
  "ai_answer": "{\"sentiment\": \"positive\"}",
  "ai_status": "ok",
  "ai_message": "Done."
}
```

*Format illustration.* Also saved: `results.csv` (UTF-8), `results.json` and a run summary (`OUTPUT`) with rows per status, charged rows, model and tokens.

| `ai_status` | Meaning | Charged |
|---|---|---|
| `ok` | Answered | yes |
| `failed` | No usable answer after 2 tries | no |
| `not_processed_limit` | Stopped by a limit (cost, free plan) | no |
| `quota_exhausted` | Daily AI capacity used up (resets 00:00 UTC) | no |
| `skipped_empty` | Nothing to send | no |

### Which model should I pick?

- **Standard** (Gemma 4 26B): fast, good for classifying, extracting and short writing. $0.002 per row.
- **Large** (Llama 3.3 70B): stronger reasoning for harder or longer rows; uses at most 12,000 characters in and 800 tokens out per row. $0.005 per row.

Both run on Cloudflare Workers AI; the run summary names the model used.

### How much does bulk AI processing cost?

| Event | Price |
|---|---|
| Row processed, standard model | $0.002 ($2 per 1,000) |
| Row processed, large model | $0.005 ($5 per 1,000) |

**Example:** classifying 5,000 reviews with the standard model = **$10**, model included.

#### What you are not charged for

Failed rows, empty rows, rows stopped by a limit (maximum cost per run, free-plan limits, daily capacity) and rows beyond Maximum rows. Each row is checked against your maximum cost per run before it is sent.

### Use it from n8n, Make or Zapier

Call the Actor with Apify's integration or the Run Actor API and pass the rows of your previous step directly:

```json
{
  "rows": [{ "company": "Example Bakery", "city": "Ghent" }],
  "promptTemplate": "Write one friendly opening sentence for an email to {{company}} in {{city}}.",
  "maxRows": 500
}
```

Then read the dataset items (same order as your rows) or download `results.csv`.

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("olliolli/bulk-ai-prompt").call(run_input={
    "fileUrl": "https://docs.google.com/spreadsheets/d/…",
    "promptTemplate": "Classify the sentiment: {{review}}",
    "outputFields": ["sentiment"],
})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(row["row_number"], row.get("ai_sentiment"))
```

Through Apify's **MCP** server, AI assistants can hand a whole spreadsheet to this Actor.

### Privacy

The text of each row (after filling in the prompt) is sent to **Cloudflare Workers AI** to get the answer; nothing else is sent there. Nothing is kept after the run except your own dataset and key-value store. Row text and answers are never written to the log. Don't send data you are not allowed to share with a processor such as Cloudflare.

### Good to know

- **Free Apify plan:** at most 20 rows per run and 100 rows per day. Paid plans don't have these limits.
- Rows are processed 4 at a time. An invalid JSON answer is retried once.
- Answers come from a language model: check a sample before you use them.

### FAQ

**Is this ChatGPT?** No. It runs open models (Gemma 4 26B and Llama 3.3 70B) in the same chat-style way, without an OpenAI account or API key.

**Can I get the answer split into columns?** Yes: list output fields and each becomes an `ai_<field>` column.

**Does it work with Google Sheets?** Yes, with a link shared as "Anyone with the link". The tab in the link is used.

**What are the free-plan limits?** 20 rows per run and 100 rows per day.

**Can I use it in n8n, Make or Zapier?** Yes, pass your rows as JSON in `rows`.

**Is my data used to train models?** Rows are processed by Cloudflare Workers AI under Cloudflare's terms; this Actor keeps nothing after the run.

### Need help?

Open an issue on the **Issues** tab.

# Actor input Schema

## `promptTemplate` (type: `string`):

The instruction the AI model gets for each row. Put column names in double curly braces, e.g. {{review}} or {{product_name}}; they are replaced by the row's values. Names are not case-sensitive, and {{address.city}} reaches into nested values. Without any {{placeholder}}, the whole row is added to the prompt as JSON.

## `outputFields` (type: `array`):

Ask the model for exactly these fields, e.g. sentiment, reason, category. Each field becomes its own column ai\_<field> (missing values are empty). Leave empty to get one plain-text answer in the column ai_answer.

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

Your data as a JSON array of objects, one object per row, e.g. \[{"review": "Arrived fast, works well"}]. Handy for API calls and quick tests. Not used when you also pick a file link or a dataset.

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

A public link to a .csv, .tsv or .xlsx file (first sheet, first row = column names), or a Google Sheets link shared as "Anyone with the link" (the tab in the link is used). Google Drive, Dropbox and OneDrive share links work too. Max. 50 MB.

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

Use the items of a dataset as rows, e.g. the output of another actor. The actor only gets read access to the dataset you pick.

## `systemPrompt` (type: `string`):

General instructions that apply to every row, e.g. "You are a careful e-commerce analyst. Answer in English." Leave empty for a short default that asks for the answer only.

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

Standard is a fast mid-size model, fine for classifying, extracting and short texts. Large is a 70B model for harder instructions. Both run on Cloudflare Workers AI; the price per row includes the model.

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

Only the first rows up to this number are read and processed.

## `maxInputChars` (type: `integer`):

A filled-in prompt longer than this is shortened: the longest values are cut first and end in " \[...]", so your instruction stays intact. The run summary counts how often this happened.

## `maxOutputTokens` (type: `integer`):

Upper limit for the model's answer per row (a token is roughly 3/4 of a word). Raise it for longer texts.

## `temperature` (type: `number`):

0 = the most predictable answers (best for classifying and extracting), higher = more varied wording (for creative lines).

## `includeOriginalColumns` (type: `boolean`):

On: every output row has your original columns plus the AI columns. Off: only row_number and the AI columns.

## Actor input object example

```json
{
  "promptTemplate": "Classify the sentiment of this review as positive, neutral or negative: {{review}}",
  "outputFields": [
    "sentiment"
  ],
  "rows": [
    {
      "product": "Desk lamp",
      "review": "Bright, easy to set up and it arrived a day early. Very happy with it."
    },
    {
      "product": "Phone case",
      "review": "It fits, but the colour is duller than in the photos."
    },
    {
      "product": "Kettle",
      "review": "Stopped working after two weeks and support never replied."
    }
  ],
  "model": "standard",
  "maxRows": 100,
  "maxInputChars": 6000,
  "maxOutputTokens": 400,
  "temperature": 0.2,
  "includeOriginalColumns": true
}
```

# Actor output Schema

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

One row per input row, in the same order: row_number, your columns, the AI answer (ai_answer or ai\_<field>), ai_status and ai_message.

## `resultsCsv` (type: `string`):

All rows with your columns and the AI columns as one CSV file (UTF-8, opens in Excel and Google Sheets).

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

Rows read and processed, rows per status, charged rows, model, tokens and warnings (OUTPUT).

# 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 = {
    "promptTemplate": "Classify the sentiment of this review as positive, neutral or negative: {{review}}",
    "outputFields": [
        "sentiment"
    ],
    "rows": [
        {
            "product": "Desk lamp",
            "review": "Bright, easy to set up and it arrived a day early. Very happy with it."
        },
        {
            "product": "Phone case",
            "review": "It fits, but the colour is duller than in the photos."
        },
        {
            "product": "Kettle",
            "review": "Stopped working after two weeks and support never replied."
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("olliolli/bulk-ai-prompt").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 = {
    "promptTemplate": "Classify the sentiment of this review as positive, neutral or negative: {{review}}",
    "outputFields": ["sentiment"],
    "rows": [
        {
            "product": "Desk lamp",
            "review": "Bright, easy to set up and it arrived a day early. Very happy with it.",
        },
        {
            "product": "Phone case",
            "review": "It fits, but the colour is duller than in the photos.",
        },
        {
            "product": "Kettle",
            "review": "Stopped working after two weeks and support never replied.",
        },
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("olliolli/bulk-ai-prompt").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 '{
  "promptTemplate": "Classify the sentiment of this review as positive, neutral or negative: {{review}}",
  "outputFields": [
    "sentiment"
  ],
  "rows": [
    {
      "product": "Desk lamp",
      "review": "Bright, easy to set up and it arrived a day early. Very happy with it."
    },
    {
      "product": "Phone case",
      "review": "It fits, but the colour is duller than in the photos."
    },
    {
      "product": "Kettle",
      "review": "Stopped working after two weeks and support never replied."
    }
  ]
}' |
apify call olliolli/bulk-ai-prompt --silent --output-dataset

```

## MCP server setup

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

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/r62aXziFR0L7dc3Ic/builds/8dAYd0rX0wf3NX009/openapi.json
