# Bulk LLM Runner — ChatGPT, Claude & Gemini on CSV & Datasets (`saliga/bulk-llm-runner`) Actor

Run a prompt on every row of a CSV, Excel file, Google Sheet or Apify dataset with ChatGPT, Claude, Gemini, DeepSeek or any OpenRouter model. Answers as new columns or typed JSON fields. No API key needed. $1 per 1,000 rows.

- **URL**: https://apify.com/saliga/bulk-llm-runner.md
- **Developed by:** [Salih Küçükkayış](https://apify.com/saliga) (community)
- **Categories:** AI, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 row answereds

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 a prompt on every row of a **CSV, Excel file, Google Sheet or Apify dataset** with ChatGPT, Claude, Gemini, DeepSeek, Perplexity or any of the 400+ models on OpenRouter, and get the answers as new columns: free text, or **typed JSON fields** (category, score, yes/no, lists) ready for your spreadsheet. Classify leads, summarise pages, extract fields, translate, tag reviews, score companies. No OpenAI, Anthropic or Google API key needed.

**$1 per 1,000 rows** (one row answered by one model), plus the model's own cost at OpenRouter's price. Failed rows are free. No platform usage fees.

### Quick start

Press **Start** with the example input. It asks the default model (GPT-6 Luna) one question about each of two companies:

```json
{
  "rows": [
    { "name": "Notion", "description": "The connected workspace for docs, wikis and projects." },
    { "name": "Figma", "description": "Figma helps teams create, test and ship better designs." }
  ],
  "prompt": "In one short sentence, what does this company do and who are its customers? Company: {{name}}. Website text: {{description}}"
}
```

You get your rows back with the answer next to them:

```json
[
  {
    "name": "Notion",
    "description": "The connected workspace for docs, wikis and projects.",
    "model": "openai/gpt-6-luna",
    "llmOutput": "…the model's answer…",
    "llmCostUsd": …,
    "tokens": …,
    "llmError": null
  }
]
```

(The shape of a row; the answer and cost depend on the model.)

Typed columns instead of text: add output fields, one per line.

```json
{
  "fileUrl": "https://docs.google.com/spreadsheets/d/…/edit",
  "prompt": "Company: {{name}}. Description: {{description}}",
  "outputFields": [
    "industry: one or two words",
    "is_b2b (boolean): sells mainly to companies",
    "employees_estimate (integer)",
    "keywords (list): three keywords"
  ]
}
```

Each field becomes its own column (`industry`, `is_b2b`, `employees_estimate`, `keywords`), using the models' structured output where they support it and a strict JSON instruction for the others.

### What you get

- **Any table in, the same table out with answers.** Apify datasets (pick one from any earlier run), CSV, TSV, Excel, JSON and JSONL links, Google Sheets, Google Drive and Dropbox share links, or rows pasted as JSON. Input columns are kept.
- **Prompt templates.** `{{column}}`, `{{nested.field}}` or `{{row}}` (the whole row as JSON).
- **Typed JSON columns** with `outputFields`: string, number, integer, boolean or list, with light type fixes ("7" becomes 7).
- **Any model, or several at once.** OpenAI, Anthropic, Google, DeepSeek, Mistral, Perplexity, xAI and more through Apify's OpenRouter Actor. List several models to get one row per model for a side-by-side comparison.
- **Web search** (optional) with the cited pages in each row.
- **Cost in every row** (`llmCostUsd`) and in total in the summary.

### Typical uses

- **Lead enrichment:** industry, size, B2B or B2C and a one-line pitch for every company in a scraped list.
- **Classify and tag:** support tickets, reviews, job posts, products.
- **Summarise:** pages from a crawl, documents converted to text, transcripts.
- **Extract fields** from free text into columns.
- **Translate or rewrite** product descriptions in bulk.
- **Compare models** on your own data before you pick one.

Chains well with: [Document to Markdown](https://apify.com/saliga/document-to-markdown), [Audio to Text](https://apify.com/saliga/audio-to-text), [Career Site Jobs](https://apify.com/saliga/career-site-jobs), [Dataset Merge & Dedupe](https://apify.com/saliga/dataset-merge-dedupe) and any scraper: pick its dataset as the input.

### Input

| Field | Default | Meaning |
|---|---|---|
| `prompt` | | Template run on every row; `{{column}}` placeholders. |
| `dataset` | | An Apify dataset to process. |
| `fileUrl`, `sheet` | | A file or share link, and the Excel sheet. |
| `rows` | | Rows pasted as a JSON array. |
| `prompts` | | Independent prompts when you have no table. |
| `models` | `openai/gpt-6-luna` | One or more OpenRouter model ids. |
| `outputFields` | | Typed JSON columns: `name (type): description`. |
| `systemPrompt` | | Instructions for every request. |
| `outputColumn` | `llmOutput` | Column for the free-text answer. |
| `keepColumns` | `true` | Copy the input columns into the output. |
| `webSearch` | `false` | Let the model search the web first. |
| `maxRows` | `1,000` | Safety limit on the number of rows. |
| `temperature`, `maxAnswerTokens`, `concurrency` | | Model settings and parallel requests (default 8). |

### Output

Your row (when `keepColumns` is on), then:

| Field | Meaning |
|---|---|
| `model` | Model that answered |
| `llmOutput` (or your `outputColumn`) | The answer text |
| your `outputFields` | One column per field, when you asked for them |
| `citations` | Pages used, with web search |
| `llmCostUsd`, `tokens` | The model's cost for this row |
| `llmError` | Why this row failed (failed rows are free) |

Without data, each row has the `prompt` instead of input columns.

### What it costs

You pay two things: **$0.001 per answered row** to this Actor, and **the model's own cost**, which Apify's [OpenRouter Actor](https://apify.com/apify/openrouter) charges to your Apify account at OpenRouter's price. Both appear on your Apify bill; each row shows its model cost (`llmCostUsd`).

Model cost depends on the model and the length of your rows. Prices per million tokens on OpenRouter (October 2026, input / output): GPT-6 Luna $0.10 / $0.50, DeepSeek V4.1 Flash $0.15 / $0.60, Gemini 3.8 Flash $0.83 / $4.13, Claude Sonnet 5.5 $2 / $10. A row with a 300-word prompt and a 50-word answer is roughly 500 tokens: about $0.0001 with GPT-6 Luna, $0.0015 with Claude Sonnet 5.5 (estimate).

| Run | This Actor | Model cost (estimate, short rows) |
|---|---|---|
| 1,000 rows with GPT-6 Luna | $1.00 | about $0.10 |
| 1,000 rows with Claude Sonnet 5.5 | $1.00 | about $1.50 |
| 1,000 rows x 3 models (comparison) | $3.00 | sum of the three |

On Apify's free plan, Apify's OpenRouter Actor charges 10 times the model price, so a paid plan is much cheaper for bigger jobs. The run's spending limit covers this Actor's charges; `maxRows` keeps the number of model calls in check.

### Good to know

- **Retries:** a row is asked again only when the model refused before answering (rate limit, server error), which providers do not bill. A timeout, an empty or unreadable answer is not asked again, because it may already have been billed; the row gets an `llmError` and is free.
- If five rows in a row fail, or your Apify account cannot pay the model (no credit), the run stops instead of failing every row.
- Rows are sent to the model provider through OpenRouter; do not process data you may not share with them.
- If the run is moved to another server, it continues where it stopped without asking or charging rows twice.

### FAQ

**Do I need an OpenAI or Anthropic API key?** No. The models are called with your run's Apify token through Apify's OpenRouter Actor.

**Can I use a Google Sheet?** Yes: share it as "Anyone with the link can view" and paste its link in `fileUrl`.

**Can I process the results of another Actor?** Yes: pick its dataset in the `dataset` field.

**Am I charged for failed rows?** No. Only rows with an answer (and, with output fields, a valid JSON answer) are charged.

### Next steps with your results

- Join the answers back to another table, filter or remove duplicates with [Dataset Merge](https://apify.com/saliga/dataset-merge-dedupe).
- Send every row to your CRM, webhook or any API: give this run's dataset to [Bulk HTTP Requests](https://apify.com/saliga/bulk-http-requests).
- Track how ChatGPT, Claude, Gemini and Perplexity talk about your brand with [AI Visibility Tracker](https://apify.com/saliga/ai-visibility-tracker).

### More from this developer

- [AI Visibility Tracker](https://apify.com/saliga/ai-visibility-tracker): how often ChatGPT, Claude, Gemini and Perplexity mention and cite your brand vs competitors, $15 per 1,000 checks.
- [Dataset Merge & Dedupe](https://apify.com/saliga/dataset-merge-dedupe): merge, deduplicate, filter and join datasets, CSV and Excel files.
- [Document to Markdown](https://apify.com/saliga/document-to-markdown): PDF, Word, PowerPoint, Excel and scanned images to clean Markdown, $2 per 1,000 pages.
- [Bulk HTTP Requests](https://apify.com/saliga/bulk-http-requests): call any API once for every row of a CSV, JSON list or Apify dataset.

Code examples (curl, Python, Node.js, n8n) for this and the other Actors: [github.com/msalihk/apify-actor-examples](https://github.com/msalihk/apify-actor-examples).

Questions or a missing feature: open an issue on the Actor page.

# Actor input Schema

## `prompt` (type: `string`):

The prompt to run on every row. Use {{column}} for a column's value ({{address.city}} for nested fields, {{row}} for the whole row as JSON). Without a dataset, file or rows, it is run once (or use Prompts below).

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

Run the prompt on every item of a dataset, for example the results of a scraper run.

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

A CSV, TSV, Excel (.xlsx), JSON or JSONL link, or a Google Sheet / Google Drive / Dropbox share link (shared as "Anyone with the link").

## `sheet` (type: `string`):

Sheet name for Excel files (default: the first sheet).

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

Or paste rows as a JSON array of objects.

## `prompts` (type: `array`):

A list of independent prompts, one per line, when you have no table to process.

## `models` (type: `array`):

OpenRouter model ids. Several models run every row on each, for a side-by-side comparison. Default openai/gpt-6-luna (fast and cheap). Others: anthropic/claude-sonnet-5.5, google/gemini-3.8-flash, deepseek/deepseek-v4.1-flash, perplexity/sonar, or any on openrouter.ai/models.

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

Optional: get typed columns instead of free text. One field per line: "name: description" or "name (type): description". Types: string, number, integer, boolean, list. Example: "industry: one or two words", "is_b2b (boolean): sells to companies".

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

Optional instructions for every request (tone, language, rules).

## `outputColumn` (type: `string`):

Column for the free-text answer.

## `keepColumns` (type: `boolean`):

Copy every input column into the output row next to the answer.

## `webSearch` (type: `boolean`):

Let the model search the web first (adds the provider's search fee, about $0.005-0.014 per answer, and citations).

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

Safety limit: the run stops before starting if the data has more rows. Every row also costs the model's own price.

## `temperature` (type: `string`):

Optional, 0-2. Lower is more consistent.

## `maxAnswerTokens` (type: `integer`):

Longer answers cost more.

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

Requests at the same time.

## Actor input object example

```json
{
  "prompt": "In one short sentence, what does this company do and who are its customers? Company: {{name}}. Website text: {{description}}",
  "rows": [
    {
      "name": "Notion",
      "description": "The connected workspace for docs, wikis and projects."
    },
    {
      "name": "Figma",
      "description": "Figma helps teams create, test and ship better designs."
    }
  ],
  "models": [
    "openai/gpt-6-luna"
  ],
  "outputColumn": "llmOutput",
  "keepColumns": true,
  "webSearch": false,
  "maxRows": 1000,
  "maxAnswerTokens": 1000,
  "concurrency": 8
}
```

# Actor output Schema

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

Your rows with the model's answer (or JSON fields), model, cost and error.

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

Rows answered and failed, model cost.

# 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 = {
    "prompt": "In one short sentence, what does this company do and who are its customers? Company: {{name}}. Website text: {{description}}",
    "rows": [
        {
            "name": "Notion",
            "description": "The connected workspace for docs, wikis and projects."
        },
        {
            "name": "Figma",
            "description": "Figma helps teams create, test and ship better designs."
        }
    ],
    "models": [
        "openai/gpt-6-luna"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("saliga/bulk-llm-runner").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 = {
    "prompt": "In one short sentence, what does this company do and who are its customers? Company: {{name}}. Website text: {{description}}",
    "rows": [
        {
            "name": "Notion",
            "description": "The connected workspace for docs, wikis and projects.",
        },
        {
            "name": "Figma",
            "description": "Figma helps teams create, test and ship better designs.",
        },
    ],
    "models": ["openai/gpt-6-luna"],
}

# Run the Actor and wait for it to finish
run = client.actor("saliga/bulk-llm-runner").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 '{
  "prompt": "In one short sentence, what does this company do and who are its customers? Company: {{name}}. Website text: {{description}}",
  "rows": [
    {
      "name": "Notion",
      "description": "The connected workspace for docs, wikis and projects."
    },
    {
      "name": "Figma",
      "description": "Figma helps teams create, test and ship better designs."
    }
  ],
  "models": [
    "openai/gpt-6-luna"
  ]
}' |
apify call saliga/bulk-llm-runner --silent --output-dataset

```

## MCP server setup

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

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/JNbczzoUoYB6xH5oF/builds/0eYXfMTfFkuLYGm7a/openapi.json
