# Dataset Join & Merge (VLOOKUP for Datasets) (`nerolabs/dataset-join-merge`) Actor

Join two datasets (or JSON arrays) on a key field like SQL or VLOOKUP: inner, left, right, full, anti joins and union. Enrich scraped leads with CRM data, combine two scrapers' output, find rows in one list missing from another, then download as CSV/Excel.

- **URL**: https://apify.com/nerolabs/dataset-join-merge.md
- **Developed by:** [Adam Pearce](https://apify.com/nerolabs) (community)
- **Categories:** Developer tools, Automation, Agents
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 joined rows

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/platform/actors/running/actors-in-store#pay-per-event

## 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

**Join two datasets on a shared key, like a SQL join or a spreadsheet VLOOKUP, without writing a script.** Point it at two Apify datasets (or paste two JSON arrays), name the key field (`email`, `sku`, `id`, `url`, or several fields together), pick a join type, and get one combined table back: every left row enriched with the matching right columns, only the rows that matched, everything from both sides, or just the rows on one side with **no** match on the other. Download the result as a real CSV or Excel file.

### Why use Dataset Join & Merge?

Every scraping or data project ends up with two tables that need to become one, and Apify's storage can't do that on its own:

- **Enrich a lead list**: scraped companies on the left, your CRM export on the right, joined on domain or email, so you can see plan, owner, or last contact next to each lead.
- **Combine two scrapers' output**: product listings from one Actor and reviews or prices from another, joined on product URL or SKU.
- **Find what's missing**: a **left anti join** returns the rows in list A that do not appear in list B. New leads not yet in your CRM. Products on a competitor's site you don't stock. Contacts who haven't replied.
- **Reconcile two exports from different systems** that use different field names for the same key (`email` on one side, `contact_email` on the other). Supported directly.
- **Stack two datasets into one** (Union mode) when they have the same shape and you just want one table.

No scraping involved at all, it only processes data you already have, so there's nothing to break when a website changes and nothing to worry about on data-source terms.

### How to use it

1. Pick your two tables: an existing Apify dataset for **Left dataset** and **Right dataset**, or paste JSON arrays into **Left data (inline)** / **Right data (inline)**.
2. Set **Key field(s) on the left** to whatever identifies a row (e.g. `email`). If the right side calls it something else, set **Key field(s) on the right** too (e.g. `contact_email`). Use several fields for a composite key (`firstName` + `lastName`).
3. Choose a **Join type**. The default, **Left join**, keeps every left row and adds the matching right columns, which is exactly what a VLOOKUP does.
4. Run it. The joined rows land in the dataset; turn on **Export result as file** for a ready-to-open CSV or Excel download.

### Join types, in plain English

| Join type | Keeps | Typical use |
|---|---|---|
| **Left join** (default) | Every left row, plus right columns where a match exists | Enrich a list (VLOOKUP) |
| **Inner join** | Only rows that exist on both sides | Overlap between two lists |
| **Right join** | Every right row, plus left columns where a match exists | Same as left, mirrored |
| **Full outer join** | Everything from both sides, matched where possible | One master table |
| **Left anti join** | Only left rows with **no** match on the right | "Who's in A but not in B?" |
| **Right anti join** | Only right rows with **no** match on the left | The reverse |
| **Union** | Every row from both sides, stacked (no key needed) | Combine two same-shape datasets |

### Input

- **Left dataset / Right dataset**: pick existing Apify datasets via the resource picker (limited-permissions safe, the Actor can only read the datasets you point at).
- **Left data / Right data (inline)**: paste a JSON array directly instead, for one-off joins.
- **Key field(s) on the left / right**: the field(s) to match on. Right key fields default to the same names as the left.
- **Join type**: see the table above.
- **Key matching**: **Normalized** (default) matches case-insensitively, trims whitespace, and treats `123` and `"123"` as equal, which is what you want for emails, names and IDs coming from two different systems. **Exact** is strict.
- **Right fields to bring in**: optionally only copy specific right columns (like choosing which VLOOKUP columns to return).
- **When a field exists on both sides**: prefix the right one (`right_price`), keep left, or keep right.
- **If a key matches several right rows**: **All** fans out like a SQL join (one output row per pair); **First** behaves like a spreadsheet VLOOKUP.
- **Add join-status fields**: on by default, adds `_joinStatus` (`matched`, `left_only`, `right_only`) and `_matchCount` to every row so you can filter afterwards. Turn off for a clean output with only your own columns.
- **Export result as file**: optional CSV and/or Excel download.

### Output

One row per joined (or passed-through) record, for example a left join of a customer list onto a plan lookup:

```json
{
  "email": "BEN@example.com",
  "name": "Ben Okafor",
  "company": "Okafor & Co",
  "plan": "Starter",
  "mrr": 19,
  "_joinStatus": "matched",
  "_matchCount": 1
}
```

The run's key-value store also holds a `JOIN_SUMMARY` record with match rates for both sides, counts of matched / unmatched rows, and any warnings (a misspelled key field, rows missing their key, a right side that isn't actually unique). You can download the dataset in various formats such as JSON, CSV, or Excel directly from the Output tab, or turn on the built-in export for a ready-to-open file.

### Pricing

Pay-per-event, anchored the same way as every low-maintenance data tool in this line: **$0.002 per joined row** (a matched left+right pair, the enriched row you actually wanted), **$0.001 per passthrough row** (an unmatched row kept by a left/right/full/anti join, or any row in Union mode), and **$0.01 per file export**. Enriching a 1,000-row lead list where 800 rows find a CRM match costs about **$1.80**, with the CSV download adding a cent. A 5,000-row anti join ("which of these aren't in my CRM yet") that returns 400 unmatched rows costs about **$0.40**. There's no separate platform-usage charge; it's included.

### Tips

- If your match rate comes back lower than expected, check the run's warnings first: a misspelled key field name, or a right side where the key field is missing on many rows, are the two usual causes. The Actor tells you explicitly when **none** of the rows on one side have the key field at all.
- Use **Normalized** key matching (the default) unless you specifically need strict matching. Two exports of "the same" email column almost always differ in case or trailing whitespace.
- If the right side should be a unique lookup table but the warnings say keys repeat, either run it through a dedupe step first or set **If a key matches several right rows** to **First** so each left row comes out exactly once.
- Turn **Add join-status fields** off when you'll feed the output straight into another tool that expects only your own columns.

### FAQ

**Can I join on a field that has a different name on each side?** Yes. Set **Key field(s) on the left** to e.g. `email` and **Key field(s) on the right** to e.g. `contact_email`. The output keeps the left name.

**What if a left row matches several right rows?** With the default (**All**) you get one output row per match, exactly like SQL, and `_matchCount` tells you how many there were. Choose **First** for spreadsheet-style VLOOKUP behaviour.

**Does this deduplicate?** No, it joins. If you need deduplication first, run your dataset through a dedupe/cleaning tool and then join the cleaned output here.

**Is my data safe?** This Actor never sends your data anywhere outside your own Apify account's storage. It doesn't scrape, doesn't call any external API, and doesn't retain anything beyond the run's own output.

If this saved you a manual VLOOKUP or a one-off join script, a review on the Store page helps a lot. Found a bug or want a feature? Use the Issues tab, replies come from a real person, usually within hours.

# Actor input Schema

## `leftDatasetId` (type: `string`):

Pick an existing Apify dataset for the LEFT side of the join, the table whose rows you want to keep and enrich. Use this OR 'Left data (inline)' below, not both. Declaring it this way is what lets this Actor run with limited permissions: it may read the dataset you point at, and nothing else on your account.

## `leftData` (type: `array`):

A JSON array of records for the LEFT side, for ad-hoc data instead of a dataset ID.

## `rightDatasetId` (type: `string`):

Pick an existing Apify dataset for the RIGHT side, the table you want to pull matching columns from. Use this OR 'Right data (inline)' below, not both.

## `rightData` (type: `array`):

A JSON array of records for the RIGHT side, for ad-hoc data instead of a dataset ID.

## `leftKeyFields` (type: `array`):

The field name(s) on the LEFT rows to match on (e.g. 'email', 'sku', 'id', or several fields together like 'firstName' + 'lastName'). Required for every join type except Union.

## `rightKeyFields` (type: `array`):

Optional. The matching field name(s) on the RIGHT rows, in the same order as the left key fields. Leave empty if the right side uses the same field names as the left (e.g. 'email' on both).

## `joinType` (type: `string`):

Which rows to keep. 'Left join' keeps every left row and adds right columns where a match exists (the VLOOKUP / enrichment case). 'Inner' keeps only rows that match on both sides. 'Full' keeps everything from both sides. 'Left anti' keeps only left rows with NO match on the right (e.g. leads not yet in your CRM). 'Right anti' is the reverse. 'Union' stacks both tables into one (no key needed).

## `keyMatching` (type: `string`):

'Normalized' matches keys case-insensitively, ignoring surrounding and repeated whitespace, and treats the number 123 and the text '123' as the same, which is what you usually want for emails, names and IDs coming from two different systems. 'Exact' requires byte-for-byte identical values.

## `rightFields` (type: `array`):

Optional. Only copy these specific fields from the matching right row (like choosing which VLOOKUP columns to return). Leave empty to bring in every right field.

## `onFieldConflict` (type: `string`):

What to do when a right field has the same name as a left field (other than the key itself). 'Prefix right' keeps both by renaming the right one (e.g. 'right\_price'). 'Keep left' ignores the right value. 'Keep right' overwrites the left value.

## `rightFieldPrefix` (type: `string`):

Used only when 'Prefix the right field' is selected above.

## `multipleMatches` (type: `string`):

'All' outputs one row per matching pair, exactly like a SQL join (a left row matching 3 right rows becomes 3 output rows). 'First' keeps only the first matching right row, exactly like a spreadsheet VLOOKUP.

## `includeJoinInfo` (type: `boolean`):

When on, every output row gets '\_joinStatus' ('matched', 'left\_only' or 'right\_only'; 'left'/'right' in Union mode) and '\_matchCount' (how many rows on the other side matched). Handy for filtering afterwards. Turn off for a clean output with only your own columns.

## `exportFormats` (type: `array`):

Optional. Generate a real downloadable file of the joined result on top of the dataset. Leave empty to skip file export entirely.

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

Safety cap on how many rows to load from each side. Leave at 0 for no cap (up to the Actor's own 100,000-rows-per-side hard limit).

## Actor input object example

```json
{
  "leftData": [
    {
      "email": "ana@example.com",
      "name": "Ana Silva",
      "company": "Silva Ltd"
    },
    {
      "email": "BEN@example.com",
      "name": "Ben Okafor",
      "company": "Okafor & Co"
    },
    {
      "email": "cara@example.com",
      "name": "Cara Lind",
      "company": "Lind Studio"
    },
    {
      "email": "dev@example.com",
      "name": "Dev Patel",
      "company": "Patel Group"
    }
  ],
  "rightData": [
    {
      "email": "ana@example.com",
      "plan": "Pro",
      "mrr": 49
    },
    {
      "email": "ben@example.com ",
      "plan": "Starter",
      "mrr": 19
    },
    {
      "email": "dev@example.com",
      "plan": "Pro",
      "mrr": 49
    },
    {
      "email": "erin@example.com",
      "plan": "Enterprise",
      "mrr": 499
    }
  ],
  "leftKeyFields": [
    "email"
  ],
  "joinType": "left",
  "keyMatching": "normalized",
  "onFieldConflict": "prefixRight",
  "rightFieldPrefix": "right_",
  "multipleMatches": "all",
  "includeJoinInfo": true,
  "exportFormats": [],
  "maxItems": 0
}
```

# Actor output Schema

## `joinedRows` (type: `string`):

The result of the join: one row per matched pair, plus unmatched rows where the join type keeps them.

## `csvFile` (type: `string`):

A ready-to-open CSV file of the joined result, if requested.

## `xlsxFile` (type: `string`):

A ready-to-open Excel (.xlsx) file of the joined result, if requested.

## `joinSummary` (type: `string`):

Counts of matched, left-only and right-only rows, match rates, and any warnings from this run.

# 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 = {
    "leftData": [
        {
            "email": "ana@example.com",
            "name": "Ana Silva",
            "company": "Silva Ltd"
        },
        {
            "email": "BEN@example.com",
            "name": "Ben Okafor",
            "company": "Okafor & Co"
        },
        {
            "email": "cara@example.com",
            "name": "Cara Lind",
            "company": "Lind Studio"
        },
        {
            "email": "dev@example.com",
            "name": "Dev Patel",
            "company": "Patel Group"
        }
    ],
    "rightData": [
        {
            "email": "ana@example.com",
            "plan": "Pro",
            "mrr": 49
        },
        {
            "email": "ben@example.com ",
            "plan": "Starter",
            "mrr": 19
        },
        {
            "email": "dev@example.com",
            "plan": "Pro",
            "mrr": 49
        },
        {
            "email": "erin@example.com",
            "plan": "Enterprise",
            "mrr": 499
        }
    ],
    "leftKeyFields": [
        "email"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("nerolabs/dataset-join-merge").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 = {
    "leftData": [
        {
            "email": "ana@example.com",
            "name": "Ana Silva",
            "company": "Silva Ltd",
        },
        {
            "email": "BEN@example.com",
            "name": "Ben Okafor",
            "company": "Okafor & Co",
        },
        {
            "email": "cara@example.com",
            "name": "Cara Lind",
            "company": "Lind Studio",
        },
        {
            "email": "dev@example.com",
            "name": "Dev Patel",
            "company": "Patel Group",
        },
    ],
    "rightData": [
        {
            "email": "ana@example.com",
            "plan": "Pro",
            "mrr": 49,
        },
        {
            "email": "ben@example.com ",
            "plan": "Starter",
            "mrr": 19,
        },
        {
            "email": "dev@example.com",
            "plan": "Pro",
            "mrr": 49,
        },
        {
            "email": "erin@example.com",
            "plan": "Enterprise",
            "mrr": 499,
        },
    ],
    "leftKeyFields": ["email"],
}

# Run the Actor and wait for it to finish
run = client.actor("nerolabs/dataset-join-merge").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 '{
  "leftData": [
    {
      "email": "ana@example.com",
      "name": "Ana Silva",
      "company": "Silva Ltd"
    },
    {
      "email": "BEN@example.com",
      "name": "Ben Okafor",
      "company": "Okafor & Co"
    },
    {
      "email": "cara@example.com",
      "name": "Cara Lind",
      "company": "Lind Studio"
    },
    {
      "email": "dev@example.com",
      "name": "Dev Patel",
      "company": "Patel Group"
    }
  ],
  "rightData": [
    {
      "email": "ana@example.com",
      "plan": "Pro",
      "mrr": 49
    },
    {
      "email": "ben@example.com ",
      "plan": "Starter",
      "mrr": 19
    },
    {
      "email": "dev@example.com",
      "plan": "Pro",
      "mrr": 49
    },
    {
      "email": "erin@example.com",
      "plan": "Enterprise",
      "mrr": 499
    }
  ],
  "leftKeyFields": [
    "email"
  ]
}' |
apify call nerolabs/dataset-join-merge --silent --output-dataset

```

## MCP server setup

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

```

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/VmoIaJsmpdovR7dzj/builds/kFgI8K8wGt3yMTH6A/openapi.json
