# Dataset Enrich — structure any dataset with your own LLM key (`multiplex/dataset-enrich`) Actor

Structure and enrich any dataset with YOUR OWN LLM key (OpenAI, Anthropic or OpenRouter). Define a target JSON schema and an instruction; every item is transformed to match, with JSON repair, retries and per-item error isolation. You control model choice and token cost.

- **URL**: https://apify.com/multiplex/dataset-enrich.md
- **Developed by:** [Daniel James](https://apify.com/multiplex) (community)
- **Categories:** AI, Developer tools
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Dataset Enrich — structure any dataset with your own LLM key

Point this at a messy dataset, describe the shape you want as a **JSON Schema**, and get back clean, typed, one-row-per-item data — structured by **your** model on **your** API key.

That last part is the whole idea. You bring an OpenAI, Anthropic or OpenRouter key; you pick the model; the tokens are billed straight to your own provider account at your own rate. This actor is the thin, careful machinery around the call — batching, concurrency, rate-limit backoff, JSON repair and a dataset that exports cleanly — and nothing else. **You control the cost and you control the model.**

### What you get

You define the columns. This is a target schema:

```json
{
  "type": "object",
  "properties": {
    "colour":    { "type": ["string", "null"] },
    "material":  { "type": ["string", "null"] },
    "size":      { "type": ["string", "null"] },
    "priceGbp":  { "type": ["number", "null"] },
    "shipsFrom": { "type": ["string", "null"] }
  },
  "required": ["colour", "priceGbp"]
}
```

and this is what lands in the dataset, one row per input item:

```json
{
  "colour": "blue",
  "material": "cotton",
  "size": "L",
  "priceGbp": 22.5,
  "shipsFrom": "Leeds",
  "_index": 0,
  "_sourceId": "sku-1"
}
```

Your fields sit at the **top level** — so the dataset exports to CSV or Excel as the flat table you designed, with no unwrapping. The only additions are `_index` (position in the input) and `_sourceId` (the item's own `id`/`url`/`sku` where it had one), which are there so you can join the enriched rows back to the source. Add `_source` too with `includeSourceItem` if you would rather carry the original row along.

### What you can do with it

- **Normalise a scrape.** Free-text product blurbs, job descriptions, listings or reviews into typed columns.
- **Classify at scale.** Sentiment, category, intent, priority, lead quality — one enum field in your schema.
- **Extract entities.** Company, location, salary band, contact type, part number — from prose that has no fields at all.
- **Translate or rewrite** into a fixed shape, then export.
- **Chain it.** Run any other actor, then feed its dataset ID straight in here and get a structured version of it.

### Bring your own key

| Provider | Endpoint called | JSON enforcement used |
|---|---|---|
| OpenAI | `api.openai.com/v1/chat/completions` | `response_format: json_object` |
| Anthropic | `api.anthropic.com/v1/messages` | forced tool call whose input schema **is** your schema |
| OpenRouter | `openrouter.ai/api/v1/chat/completions` | `response_format: json_object` |

**Your key is handled as a secret and nothing else.** It is marked `isSecret` in the input schema, so Apify encrypts it at rest; at run time it is held in memory and put into the `Authorization` / `x-api-key` header of the call to your provider — and that is the only place it ever goes. This actor never logs it, never puts it in the run's status message, and never writes it to the dataset or to any storage of its own. Provider error bodies sometimes echo a rejected key straight back at you; every string this actor emits is scrubbed of the key before it leaves, so even that cannot leak it.

**Model ids:** give the model exactly as your provider spells it — for example `gpt-4o-mini` (OpenAI), `claude-3-5-haiku-latest` (Anthropic), `openai/gpt-4o-mini` (OpenRouter). Those are illustrations, not a supported list: providers add and retire models constantly, so check your own provider's model list. A small, cheap model is normally more than enough for structuring, and it is your bill.

### When the model misbehaves — because it will

A structuring run that dies on item 400 of 5,000 is worthless. This one does not die:

1. **Loose parsing first.** Fences, prose before or after the JSON, a single object where an array was asked for — all recovered locally, for free, without another call.
2. **One repair retry.** If it is still not JSON, the model is shown its own reply back and asked for valid JSON only. Exactly one retry: a model that cannot manage it twice will not manage it on the third attempt, and every attempt is your money.
3. **Then an honest row.** Still broken? That item gets `{"_error": "...", "_raw": "..."}` instead — the reason, and the reply that could not be parsed — and the run carries on. Check the **Errors** view in the dataset.

The same applies to the API itself: `429`s and `5xx`s are retried with exponential backoff and the provider's own `Retry-After` is honoured; a `401`, `404` or `400` is not retried at all, because a bad key or a bad model id will fail identically forever. If your model or your schema turns out not to support the provider's structured-output mode, the run notices once, drops to prompt-only JSON, and continues rather than failing every batch.

### Input

| Field | Example | Notes |
|---|---|---|
| `datasetId` | `KhNDDNIQRowKYqlD7` | the dataset to read — typically another actor's run output |
| `items` | `[{"id":"1","text":"…"}]` | paste rows in directly instead; used only when `datasetId` is empty |
| `instruction` | `"Extract the product attributes…"` | what to do with each row, in plain English. Say what to do when a value is missing |
| `outputSchema` | see above | JSON Schema for **one** output row. Its properties become your columns |
| `provider` | `openai` | `openai`, `anthropic` or `openrouter` |
| `apiKey` | `sk-…` | **your** key. Stored encrypted, never logged, never written to the dataset |
| `model` | `gpt-4o-mini` | exactly as your provider spells it |
| `batchSize` | `1` | rows per model call. Bigger = fewer tokens overall, but a longer reply to get wrong. 1–5 is the safe range |
| `concurrency` | `2` | parallel calls, capped at 5 |
| `maxItems` | `0` | 0 = everything. **Set it to 10 for a trial run first** |
| `includeSourceItem` | `false` | keep the untouched source row under `_source` |

#### Getting a good result

- **Be specific in the instruction**, and say what "unknown" looks like — *"use null for anything the text does not state; never guess"* is worth more than any schema tweak.
- **Type your schema properly.** `{"type": ["number", "null"]}` gets you a number column; `{"type": "string"}` gets you `"22.50"` as text.
- **Use an `enum`** for classification fields. It is the single biggest accuracy win available.
- **Keep the schema flat** if you plan to export to CSV.
- **Start with `maxItems: 10`.** Look at the rows, fix the instruction, then run the lot.

### Honest limits

- **Quality is your model's, not ours.** This actor guarantees the plumbing — one row in, one row out, valid JSON or an explicit error. It cannot make a weak model accurate, and it does not validate the model's output against your schema field by field: the schema steers the model and defines your columns, it is not a post-hoc assertion.
- **You pay your provider directly** for every token, including the tokens spent on a repair retry and on rows that ultimately fail. `maxItems` is your seatbelt.
- **Large batches truncate.** If a reply hits the model's output limit it arrives as broken JSON and becomes error rows. Lower `batchSize` if you see that.
- **Three providers**, on their standard endpoints. Azure OpenAI, Bedrock, Vertex and self-hosted gateways are not supported (OpenRouter covers a very wide model list if yours is not on OpenAI or Anthropic directly).
- **Row order is not guaranteed** in the dataset, because batches run concurrently. `_index` always is — sort on it.
- **Send only data you are entitled to send.** Rows go to the provider you choose, under their terms and their retention policy, not ours. If your dataset contains personal data, that is your lawful basis and your processor agreement to hold.

### Typical costs

Two separate bills, and it is worth being clear about which is which:

- **Your provider** charges you for tokens, at your rate, on your account. This is the dominant cost and it is entirely under your control — model, `batchSize` and `maxItems` are the three dials.
- **This actor** charges per enriched row. Platform compute is negligible: it is an I/O-bound HTTP loop with no browser and no proxies, and it spends nearly all of its wall time waiting on your provider. **Rows that fail are not charged** — you are billed for structured data, not for attempts.

# Actor input Schema

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

The Apify dataset to read, e.g. the default dataset of another actor's run. Leave empty and paste rows into `items` instead. Give one or the other, not both.

## `items` (type: `array`):

Rows to enrich, as a JSON array of objects. Used only when `datasetId` is empty. Handy for a trial run before you point this at a full dataset.

## `instruction` (type: `string`):

What the model should do with each row, in plain English. Be specific about how to derive each field and what to do when a value is missing.

## `outputSchema` (type: `object`):

A JSON Schema describing ONE output row. Every enriched row is produced against it, and its properties become the dataset's columns. Keep it flat and typed for the cleanest CSV/Excel export.

## `provider` (type: `string`):

Whose API to call. The key you supply below is sent only to this provider, over HTTPS, and is never written to the dataset, the logs or the key-value store.

## `apiKey` (type: `string`):

Your own API key for the provider above. Stored encrypted by Apify, held in memory only during the run, and never logged or pushed to any storage. Use a key scoped to this job and rotate it as you would any other.

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

The model id exactly as your provider spells it — e.g. `gpt-4o-mini` (OpenAI), `claude-3-5-haiku-latest` (Anthropic), `openai/gpt-4o-mini` (OpenRouter). Model ids change; check your provider's own model list. A small, cheap model is usually plenty for structuring.

## `batchSize` (type: `integer`):

How many rows to send in one model call. Larger batches cost fewer tokens overall but risk hitting the model's output limit, which shows up as repaired or failed rows. 1-5 is the safe range.

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

How many model calls to run at once. Capped at 5. Lower it if your provider returns 429s (the actor already backs off and honours Retry-After).

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

Stop after this many source rows. 0 = every row. Set it to 10 for a cheap trial run before committing your key to a full dataset.

## `includeSourceItem` (type: `boolean`):

Keep the untouched source row under `_source` on every output row. Off by default so the dataset stays narrow and exports cleanly.

## Actor input object example

```json
{
  "items": [
    {
      "id": "1",
      "text": "Blue cotton tee, mens, L, £22.50, ships from Leeds"
    }
  ],
  "instruction": "Extract the product attributes from the free-text description. Use null for anything the text does not state; never guess.",
  "outputSchema": {
    "type": "object",
    "properties": {
      "colour": {
        "type": [
          "string",
          "null"
        ]
      },
      "material": {
        "type": [
          "string",
          "null"
        ]
      },
      "size": {
        "type": [
          "string",
          "null"
        ]
      },
      "priceGbp": {
        "type": [
          "number",
          "null"
        ]
      },
      "shipsFrom": {
        "type": [
          "string",
          "null"
        ]
      }
    },
    "required": [
      "colour",
      "priceGbp"
    ]
  },
  "provider": "openai",
  "model": "gpt-4o-mini",
  "batchSize": 1,
  "concurrency": 2,
  "maxItems": 0,
  "includeSourceItem": false
}
```

# Actor output Schema

## `items` (type: `string`):

One row per input item, carrying the fields of your target JSON Schema plus \_index/\_sourceId. Rows the model could not return valid JSON for carry \_error and \_raw instead.

# 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 = {
    "items": [
        {
            "id": "1",
            "text": "Blue cotton tee, mens, L, £22.50, ships from Leeds"
        }
    ],
    "instruction": "Extract the product attributes from the free-text description. Use null for anything the text does not state; never guess.",
    "outputSchema": {
        "type": "object",
        "properties": {
            "colour": {
                "type": [
                    "string",
                    "null"
                ]
            },
            "material": {
                "type": [
                    "string",
                    "null"
                ]
            },
            "size": {
                "type": [
                    "string",
                    "null"
                ]
            },
            "priceGbp": {
                "type": [
                    "number",
                    "null"
                ]
            },
            "shipsFrom": {
                "type": [
                    "string",
                    "null"
                ]
            }
        },
        "required": [
            "colour",
            "priceGbp"
        ]
    },
    "model": "gpt-4o-mini"
};

// Run the Actor and wait for it to finish
const run = await client.actor("multiplex/dataset-enrich").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 = {
    "items": [{
            "id": "1",
            "text": "Blue cotton tee, mens, L, £22.50, ships from Leeds",
        }],
    "instruction": "Extract the product attributes from the free-text description. Use null for anything the text does not state; never guess.",
    "outputSchema": {
        "type": "object",
        "properties": {
            "colour": { "type": [
                    "string",
                    "null",
                ] },
            "material": { "type": [
                    "string",
                    "null",
                ] },
            "size": { "type": [
                    "string",
                    "null",
                ] },
            "priceGbp": { "type": [
                    "number",
                    "null",
                ] },
            "shipsFrom": { "type": [
                    "string",
                    "null",
                ] },
        },
        "required": [
            "colour",
            "priceGbp",
        ],
    },
    "model": "gpt-4o-mini",
}

# Run the Actor and wait for it to finish
run = client.actor("multiplex/dataset-enrich").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 '{
  "items": [
    {
      "id": "1",
      "text": "Blue cotton tee, mens, L, £22.50, ships from Leeds"
    }
  ],
  "instruction": "Extract the product attributes from the free-text description. Use null for anything the text does not state; never guess.",
  "outputSchema": {
    "type": "object",
    "properties": {
      "colour": {
        "type": [
          "string",
          "null"
        ]
      },
      "material": {
        "type": [
          "string",
          "null"
        ]
      },
      "size": {
        "type": [
          "string",
          "null"
        ]
      },
      "priceGbp": {
        "type": [
          "number",
          "null"
        ]
      },
      "shipsFrom": {
        "type": [
          "string",
          "null"
        ]
      }
    },
    "required": [
      "colour",
      "priceGbp"
    ]
  },
  "model": "gpt-4o-mini"
}' |
apify call multiplex/dataset-enrich --silent --output-dataset

```

## MCP server setup

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

```

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/Z3kpDHDowow98uF5u/builds/lpXTM9JCLeMfIl9kW/openapi.json
