# Merge Datasets & Dedupe: Combine, Filter, Export CSV & Excel (`data-done-right/merge-datasets`) Actor

Merge datasets API and dataset merge tool: combine datasets from any Apify run, merge CSV, JSON, JSONL and Excel files into one clean dataset. Dedupe by key or full row, dataset filter rules, flatten, rename, sort, limit, export CSV, XLSX, JSON with public links.

- **URL**: https://apify.com/data-done-right/merge-datasets.md
- **Developed by:** [Data Done Right](https://apify.com/data-done-right) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.24 / 1,000 rows

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

## Merge Datasets & Dedupe: Combine, Filter, Export CSV & Excel

**Merge datasets** from any Apify run, plus CSV, JSON, JSONL and Excel files and pasted text, into one clean dataset. This **dataset merge** Actor can **dedupe** rows by key fields or by a full-row hash, apply **dataset filter** rules (equals, contains, regex, greater/less than, between, exists, empty, AND/OR), flatten nested fields, pick and rename columns, sort, limit, and write **CSV, Excel (.xlsx), JSON and JSONL export files** with public links. Use it to **combine datasets** from several scraper runs, to **merge CSV** exports from different tools, or to clean up a dataset before you send it somewhere else. Nothing to install and no API key: it reads other datasets with your own run's access.

### What can you merge

- **Apify datasets from any Actor.** Pick datasets in the input form, or paste dataset IDs, `username~name` names or dataset links. Your own runs and other creators' runs all work, as long as you can read them.
- **CSV, TSV, JSON, JSONL and Excel (.xlsx) files** from a direct link. The type is detected from the extension or the content. CSV delimiters `, ; tab |`, UTF-8 and BOM are handled.
- **Pasted text.** Put CSV, JSON or JSONL straight into the input (`inlineSources`). It works offline and is handy for tests and for AI agents that already hold the data.
- Sources have different columns? The result has all of them. A row simply has no value for a column its source did not have.

### How to merge datasets

1. Add your sources: **Datasets to merge**, **Dataset links or IDs**, **File URLs** and/or **Pasted data**. They are read in that order.
2. Choose how to **dedupe**: by key fields (`email`, `url`, `firstName` + `lastName`...), or identical rows only.
3. Optionally add **filter rules**, columns to keep, renames and a sort order.
4. Pick **export files** (CSV, XLSX, JSON, JSONL) if you want downloadable files. Run it.
5. Read the rows from the run's dataset, and the file links and counts from the `SUMMARY` record.

### Dedupe datasets

- **By key fields:** rows with the same value in all `dedupeKeys` are duplicates. Keys are trimmed, and `42` and `"42"` count as the same value. Turn on **ignore upper/lower case** so `ANNA@x.com` equals `anna@x.com`. Dots reach nested fields (`author.id`).
- **First or last wins:** keep the first row seen (earlier sources win) or the last one. Keep-first streams through the data and stores only a 12-byte hash per row. Keep-last holds the surviving rows in memory.
- **Full-row hash:** only rows that are identical in every field are removed (key order does not matter, `1` equals `1.0`).
- **Rows with no key:** if all key fields of a row are empty or missing, the row is compared by its full content instead, so such rows are never all collapsed into one. They are only removed when they are exact copies of each other.
- This is **exact matching**. It does not detect near-duplicates such as "Jon Smith" and "John Smith".

### Dataset filter rules

Each rule is `{"field": "...", "op": "...", "value": ...}`. Rules are combined with AND (default) or OR. Filters run before dedupe, on the original field names (or the flattened names when **Flatten** is on).

| Operator | Keeps a row when... |
|---|---|
| `equals`, `notEquals` | the value equals (or does not equal) `value`. Numbers match numbers (`5` = `"5"`), text ignores case unless **Case-sensitive filters** is on. For a list field, any element may match. |
| `contains`, `notContains`, `startsWith`, `endsWith` | the text contains / starts with / ends with `value` (or any item of a list of values). |
| `regex` | the text matches the regular expression (case-insensitive by default). |
| `gt`, `gte`, `lt`, `lte` | the number or ISO date compares as stated. `"$1,299.50"` and `"2024-05-31"` are understood. Text that is not a number or date never matches. |
| `between` | the number or date is inside `[min, max]` (inclusive). Use `null` to leave one side open. |
| `in` | the value equals any item of the list. |
| `exists`, `notExists` | the field is present with a non-null value / absent or null. |
| `empty`, `notEmpty` | the field is missing, null, blank text, `[]` or `{}` / is not. |

### Input example

```json
{
  "datasetIds": ["aBcD1234eFgH5678i", "kLmN9012oPqR3456s"],
  "fileUrls": ["https://example.com/exports/leads.csv"],
  "inlineSources": [{"name": "manual", "format": "csv", "text": "email,name\nzoe@example.com,Zoe\n"}],
  "dedupeMode": "keys",
  "dedupeKeys": ["email"],
  "dedupeCaseInsensitive": true,
  "filters": [
    {"field": "plan", "op": "notEquals", "value": "free"},
    {"field": "signup", "op": "between", "value": ["2024-01-01", "2024-12-31"]}
  ],
  "flatten": true,
  "fields": ["email", "name", "company.name"],
  "renameFields": {"company.name": "company"},
  "sortBy": ["signup:desc"],
  "exports": ["csv", "xlsx"]
}
```

The default input of the Actor needs no datasets: it merges two small pasted CSV lists with overlapping emails, so you can see dedupe, a filter, a sort and both export files in one quick run.

### Output

One dataset row per merged row, with the columns of your sources (or only the ones you chose):

```json
{
  "email": "elena@example.com", "name": "Elena Rossi", "country": "IT", "plan": "pro",
  "signup": "2024-04-12", "company": "Initech", "source": "newsletter-list"
}
```

The `SUMMARY` record in the key-value store has the run report:

```json
{
  "rowsIn": 8, "rowsFiltered": 3, "duplicatesRemoved": 2, "rowsOut": 3,
  "sources": [{"name": "crm-export", "type": "inline", "rowsRead": 4, "status": "ok"}],
  "errors": [],
  "exports": [{"format": "csv", "key": "merged.csv", "url": "https://api.apify.com/v2/key-value-stores/.../records/merged.csv", "rows": 3, "bytes": 242}],
  "stoppedBySpendingLimit": false, "inputCapReached": false
}
```

- `sources` lists every source with its row count and a `status`: `ok`, `truncated` (a cap was reached), `skipped` or `error`. A failing source never stops the others: its reason is in `errors` and the run goes on.
- Export files are saved in the key-value store as `merged.csv`, `merged.xlsx`, `merged.json` and `merged.jsonl` (change the base name with **Export file name**). They contain exactly the rows that were delivered.
- CSV files are UTF-8 with a BOM so Excel opens them correctly. Nested values and lists are written as JSON text in CSV and Excel cells.

### Merge CSV and Excel files

Give direct links to `.csv`, `.tsv`, `.json`, `.jsonl` or `.xlsx` files. Excel files are read from the first sheet (or the sheet you name), the first non-empty row is the header, dates become ISO text and formulas are read as the values Excel saved. CSV numbers and booleans are converted when that is unambiguous; `007`, `+1555` and very long digit strings stay text. Links to localhost, internal names and private networks are blocked, including through redirects.

### Merge datasets API

Call this Actor from the Apify API, Python, JavaScript, Make, Zapier or n8n, for example right after a scraper run. Example with the Apify Python client:

```python
from apify_client import ApifyClient

client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("data-done-right/merge-datasets").call(run_input={
    "datasetIds": ["aBcD1234eFgH5678i", "kLmN9012oPqR3456s"],   # default datasets of two runs
    "dedupeKeys": ["url"],
    "exports": ["csv"],
})
rows = client.dataset(run["defaultDatasetId"]).list_items().items
```

In the Apify Console you can also add it as an integration of a scraper (Integrations, "Run Actor") and set the dataset to `{{resource.defaultDatasetId}}`.

### Use it in Claude, ChatGPT and Cursor

Connect Apify's MCP server (mcp.apify.com) and your AI assistant can combine data for you. For example: "merge these three dataset IDs, dedupe by url, keep only rows where price is under 50, and give me an Excel file". The inputs are `datasetIds` (or `datasetUrls`), `dedupeKeys`, `filters` and `exports`.

### Use cases

- **Combine scraper runs:** merge today's and yesterday's runs, or the same scraper run in several countries, and drop the repeats.
- **Dataset cleanup:** filter out rows with empty fields, keep only the columns you need, rename them and sort the result.
- **Mixed sources:** join a CRM export (CSV), a partner's Excel sheet and a scraper dataset into one deduplicated list.
- **Reports and handoffs:** produce a CSV or Excel file with a public link for someone who does not use Apify.

### How much does it cost

Pay per event: **$0.0003 per output row** (`row`), and **$0.005 per export file** (`export-file`) when you ask for exports. You are charged for the rows that end up in the result, once each. Rows that were read, filtered out or removed as duplicates are free, and so are the `SUMMARY` record and sources that fail. 10,000 merged rows cost about $3.00, plus $0.005 for each export file. Paid Apify plans get 10% (Silver) to 20% (Gold and up) off. If you set a maximum spend per run, the Actor stops cleanly when it is reached and keeps the rows already delivered. **Max rows to read** and **Max output rows** cap what a run can cost.

### FAQ

#### How do I merge datasets from two scraper runs?

Pick both datasets in **Datasets to merge** (or paste their IDs), set the key that identifies a row in **Dedupe key fields** and run. The Actor reads both through your run's access and writes one deduplicated dataset.

#### Can I merge a dataset with a CSV or Excel file?

Yes. Add the dataset and the file link in the same run. Columns are united: a row gets a value only for the columns its source had.

#### Which duplicate is kept?

The first one seen by default, in source order (datasets, then dataset links, then files, then pasted text). Choose **Last seen** to keep the latest instead.

#### Can it read datasets from other creators' Actors?

It can read any dataset your Apify account is allowed to read, including runs of other creators' Actors that you started. For datasets that are not yours, pick them in the **Datasets to merge** field: that is what gives a limited-permission run read access. A dataset given only as a link or ID in **Dataset links or IDs** can fail on a limited-permission run. The failure is reported for that source and the run continues.

#### What happens when one source fails?

It is listed in `errors` with the reason (not found, no access, bad file, blocked address, too large) and the other sources are still merged. If every source fails, or all sources are empty, the run fails and nothing is charged.

#### Is this an Apify dataset merger or a SQL join?

It is a merger (rows are stacked and deduplicated). It does not join two datasets on a key to combine their columns.

#### How big can the input be?

By default it reads up to 100,000 rows in total; raise **Max rows to read** up to 2,000,000. Keep-first dedupe streams and keeps only hashes. Sorting and keep-last dedupe hold the surviving rows in memory (the Actor has 1 GB by default), so use a smaller cap or more memory for very wide rows. Files are limited to 50 MB by default (500 MB max), and a single JSON array file is loaded whole and limited to 100 MB (use JSONL or CSV for more).

#### Why did my CSV export put an apostrophe in front of some cells?

A text cell starting with `=`, `+`, `-` or `@` can run as a formula when opened in Excel or Sheets. The CSV export prefixes such text with an apostrophe (plain numbers are left alone). Switch off **Protect CSV from spreadsheet formulas** if you need the raw text. Excel (.xlsx) exports always store such text as plain text, and the dataset, JSON and JSONL are never changed.

#### Does it work with Google Sheets?

Use a published CSV link or export the sheet as CSV or XLSX. A link to the normal Google Sheets page returns a web page, which the Actor rejects with a clear message.

### Limits

- Exact duplicates only (after trimming, and optional case folding). No fuzzy matching, no joins.
- Old `.xls` files are not supported (save as `.xlsx` or CSV). CSV is read as UTF-8.
- Excel exports are limited to 1,048,575 rows per sheet and 32,767 characters per cell (longer text is cut). When a result is too big for Excel, the other export formats still work.
- Regular expressions run on your own data and are limited to 1,000 characters.
- Filters, dedupe and sort use the original (or flattened) field names. Selecting and renaming columns is the last step.
- If you run it with limited permissions, datasets you do not own must be picked in **Datasets to merge**.

### Related tools

- **Google Sheets Import & Export: Apify Data, No Login**: send the merged dataset to a Google Sheet.
- **Slack Notifications: Send Data to Slack, Discord & Email**: send the new rows to your team.
- **HTTP Request Runner: Bulk API Calls, Retries, Proxies**: fetch API data to merge.

# Actor input Schema

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

Apify datasets to merge, in this order. Pick them from the list, or paste dataset IDs. Reads go through your run, so datasets of other Actors and other runs that you can read all work. Example: \["aBcD1234eFgH5678i", "username~my-named-dataset"]. Leave empty to use only files and pasted text.

## `datasetUrls` (type: `array`):

Dataset IDs, "username~name" names or Apify links, one per line. Example: https://console.apify.com/storage/datasets/aBcD1234eFgH5678i or https://api.apify.com/v2/datasets/aBcD1234eFgH5678i/items. A dataset that is only given here may be unreadable when the run has limited permissions: pick it in "Datasets to merge" instead.

## `fileUrls` (type: `array`):

Direct links to .csv, .tsv, .json, .jsonl or .xlsx files, one per line. The type is detected from the extension or the content. Example: https://example.com/export/leads.csv. Links to localhost and private networks are blocked.

## `inlineSources` (type: `array`):

CSV, JSON or JSONL text pasted straight into the input. Each item is {"name": ..., "format": "csv" | "json" | "jsonl", "text": ...}. The name is shown in the source column. Works offline, so it is a good way to try the Actor.

## `sourceField` (type: `string`):

Add a column with the name of the source each row came from (the dataset ID, file name or inline name). Leave empty for no extra column. Example: source.

## `dedupeMode` (type: `string`):

keys: rows with the same value in all Dedupe key fields are duplicates. fullRow: only identical rows are duplicates. off: keep everything.

## `dedupeKeys` (type: `array`):

Fields that identify a row, e.g. \["email"] or \["firstName", "lastName"]. Dots reach nested fields: "author.id". Values are trimmed; a number and the same number as text count as equal. A row where all key fields are empty is only removed when it is an exact copy of another row.

## `dedupeKeep` (type: `string`):

first keeps the earliest row (streams, uses little memory). last keeps the latest row and puts it where its last copy appeared (the surviving rows are held in memory).

## `dedupeCaseInsensitive` (type: `boolean`):

Treat ANNA@example.com and anna@example.com as the same key. Applies to key fields (or the whole row in full-row mode).

## `filters` (type: `array`):

Keep only rows that match. Each rule is {"field": ..., "op": ..., "value": ...}. Operators: equals, notEquals, contains, notContains, startsWith, endsWith, regex, gt, gte, lt, lte, between (value \[min, max], inclusive, dates or numbers), in (value is a list), exists, notExists, empty, notEmpty. Example: \[{"field": "plan", "op": "notEquals", "value": "free"}, {"field": "price", "op": "between", "value": \[10, 100]}]. Filters run before dedupe.

## `filterMode` (type: `string`):

and: a row must match every rule. or: matching any one rule is enough.

## `filterCaseSensitive` (type: `boolean`):

Make equals, contains, startsWith, endsWith, in and regex respect upper/lower case. Off by default.

## `flatten` (type: `boolean`):

Turn nested objects into dotted columns, e.g. {"address": {"city": "Oslo"}} becomes address.city. Lists are kept as lists (they become JSON text in CSV and Excel).

## `fields` (type: `array`):

Only these columns, in this order. Dots reach nested fields. Leave empty to keep all columns. Filter, dedupe and sort use the original names; this step comes last. Example: email, name, address.city.

## `renameFields` (type: `object`):

Rename columns in the output, as {"old name": "new name"}. Example: {"address.city": "city", "emailAddress": "email"}.

## `sortBy` (type: `array`):

Sort the output by one or more fields, one per line, with :desc for descending. Numbers sort as numbers, ISO dates as dates, text without regard to case, empty values last. Example: signup:desc. Sorting holds the surviving rows in memory.

## `limit` (type: `integer`):

Stop after this many output rows (after sorting). Leave empty for no limit. You are charged per output row.

## `exports` (type: `array`):

Also write the result as downloadable files in the key-value store, each with a public link in the SUMMARY record. Every file is charged once. Leave empty for the dataset only.

## `exportName` (type: `string`):

Base name of the export files, e.g. merged gives merged.csv and merged.xlsx.

## `escapeFormulas` (type: `boolean`):

In the CSV export, text that starts with = + - or @ gets a leading apostrophe so Excel and Sheets cannot run it as a formula. Excel files always store such text as plain text. Turn off only if you need the raw values.

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

Hard cap on rows read across all sources. Reading stops at the cap and the run still succeeds. It also bounds memory when sorting or keeping the last duplicate.

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

Optional cap on rows taken from each source, so one huge source cannot use up the whole budget. Leave empty for no per-source cap.

## `maxFileSizeMb` (type: `integer`):

Files larger than this are rejected (that source fails, the others continue). A JSON array file is loaded whole and is limited to 100 MB; use JSONL or CSV for bigger files.

## `csvInferTypes` (type: `boolean`):

Turn CSV text like 42, 3.5 or true into numbers and booleans. Values with leading zeros (007), plus signs or very long digit strings stay text.

## `xlsxSheet` (type: `string`):

Sheet name or number (1 = first) to read from every Excel file. Leave empty for the first sheet.

## Actor input object example

```json
{
  "inlineSources": [
    {
      "name": "crm-export",
      "format": "csv",
      "text": "email,name,country,plan,signup\nanna@example.com,Anna Smith,US,pro,2024-01-15\nben@example.com,Ben Jones,UK,free,2024-02-03\ncarla@example.com,Carla Diaz,US,pro,2024-02-20\ndev@example.com,Dev Patel,IN,free,2024-03-01\n"
    },
    {
      "name": "newsletter-list",
      "format": "csv",
      "text": "email,name,country,plan,signup,company\nANNA@example.com,Anna Smith,US,pro,2024-01-15,Acme\ncarla@example.com,Carla Diaz,US,enterprise,2024-04-10,Globex\nelena@example.com,Elena Rossi,IT,pro,2024-04-12,Initech\nfrank@example.com,Frank Moore,US,free,2024-05-02,Umbrella\n"
    }
  ],
  "sourceField": "source",
  "dedupeMode": "keys",
  "dedupeKeys": [
    "email"
  ],
  "dedupeKeep": "first",
  "dedupeCaseInsensitive": true,
  "filters": [
    {
      "field": "plan",
      "op": "notEquals",
      "value": "free"
    }
  ],
  "filterMode": "and",
  "filterCaseSensitive": false,
  "flatten": false,
  "sortBy": [
    "signup:desc"
  ],
  "exports": [
    "csv",
    "xlsx"
  ],
  "exportName": "merged",
  "escapeFormulas": true,
  "maxItems": 100000,
  "maxFileSizeMb": 50,
  "csvInferTypes": true
}
```

# Actor output Schema

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

The merged, deduplicated and filtered rows (all result items of this run, as JSON, CSV, Excel and more).

## `records` (type: `string`):

Key-value store records: SUMMARY (rows in and out, duplicates removed, rows filtered, per-source counts and errors, export file links) and the generated export files (CSV, XLSX, JSON, JSONL).

# 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 = {
    "inlineSources": [
        {
            "name": "crm-export",
            "format": "csv",
            "text": "email,name,country,plan,signup\nanna@example.com,Anna Smith,US,pro,2024-01-15\nben@example.com,Ben Jones,UK,free,2024-02-03\ncarla@example.com,Carla Diaz,US,pro,2024-02-20\ndev@example.com,Dev Patel,IN,free,2024-03-01\n"
        },
        {
            "name": "newsletter-list",
            "format": "csv",
            "text": "email,name,country,plan,signup,company\nANNA@example.com,Anna Smith,US,pro,2024-01-15,Acme\ncarla@example.com,Carla Diaz,US,enterprise,2024-04-10,Globex\nelena@example.com,Elena Rossi,IT,pro,2024-04-12,Initech\nfrank@example.com,Frank Moore,US,free,2024-05-02,Umbrella\n"
        }
    ],
    "sourceField": "source",
    "dedupeMode": "keys",
    "dedupeKeys": [
        "email"
    ],
    "dedupeKeep": "first",
    "dedupeCaseInsensitive": true,
    "filters": [
        {
            "field": "plan",
            "op": "notEquals",
            "value": "free"
        }
    ],
    "sortBy": [
        "signup:desc"
    ],
    "exports": [
        "csv",
        "xlsx"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("data-done-right/merge-datasets").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 = {
    "inlineSources": [
        {
            "name": "crm-export",
            "format": "csv",
            "text": """email,name,country,plan,signup
anna@example.com,Anna Smith,US,pro,2024-01-15
ben@example.com,Ben Jones,UK,free,2024-02-03
carla@example.com,Carla Diaz,US,pro,2024-02-20
dev@example.com,Dev Patel,IN,free,2024-03-01
""",
        },
        {
            "name": "newsletter-list",
            "format": "csv",
            "text": """email,name,country,plan,signup,company
ANNA@example.com,Anna Smith,US,pro,2024-01-15,Acme
carla@example.com,Carla Diaz,US,enterprise,2024-04-10,Globex
elena@example.com,Elena Rossi,IT,pro,2024-04-12,Initech
frank@example.com,Frank Moore,US,free,2024-05-02,Umbrella
""",
        },
    ],
    "sourceField": "source",
    "dedupeMode": "keys",
    "dedupeKeys": ["email"],
    "dedupeKeep": "first",
    "dedupeCaseInsensitive": True,
    "filters": [{
            "field": "plan",
            "op": "notEquals",
            "value": "free",
        }],
    "sortBy": ["signup:desc"],
    "exports": [
        "csv",
        "xlsx",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("data-done-right/merge-datasets").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 '{
  "inlineSources": [
    {
      "name": "crm-export",
      "format": "csv",
      "text": "email,name,country,plan,signup\\nanna@example.com,Anna Smith,US,pro,2024-01-15\\nben@example.com,Ben Jones,UK,free,2024-02-03\\ncarla@example.com,Carla Diaz,US,pro,2024-02-20\\ndev@example.com,Dev Patel,IN,free,2024-03-01\\n"
    },
    {
      "name": "newsletter-list",
      "format": "csv",
      "text": "email,name,country,plan,signup,company\\nANNA@example.com,Anna Smith,US,pro,2024-01-15,Acme\\ncarla@example.com,Carla Diaz,US,enterprise,2024-04-10,Globex\\nelena@example.com,Elena Rossi,IT,pro,2024-04-12,Initech\\nfrank@example.com,Frank Moore,US,free,2024-05-02,Umbrella\\n"
    }
  ],
  "sourceField": "source",
  "dedupeMode": "keys",
  "dedupeKeys": [
    "email"
  ],
  "dedupeKeep": "first",
  "dedupeCaseInsensitive": true,
  "filters": [
    {
      "field": "plan",
      "op": "notEquals",
      "value": "free"
    }
  ],
  "sortBy": [
    "signup:desc"
  ],
  "exports": [
    "csv",
    "xlsx"
  ]
}' |
apify call data-done-right/merge-datasets --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,data-done-right/merge-datasets"
        }
    }
}
```

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/vX1zBVcAoSQNlpJLI/builds/nGdevs9XU55HMjGss/openapi.json
