Join Datasets: VLOOKUP & Merge Two Datasets or CSV Files
Pricing
from $0.70 / 1,000 joined rows
Join Datasets: VLOOKUP & Merge Two Datasets or CSV Files
Join two Apify datasets or CSV, JSON or Excel files by a key, like VLOOKUP or a SQL join: add the fields of the matching lookup row to each row. Match on text, exact values or website domains; left, inner or anti join. Enrich scraper results with other actors' data. Pay per row.
Pricing
from $0.70 / 1,000 joined rows
Rating
0.0
(0)
Developer
Michael Costa
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
15 hours ago
Last modified
Categories
Share
What does Join Datasets do?
Join Datasets joins two Apify datasets or CSV, JSON or Excel files by a key, like VLOOKUP or a SQL join: each row of your main data gets the fields of its matching row in the lookup data. Match on text, exact values or website domains; keep every row (left join), only matches (inner) or only non-matches (anti).
It's the step that puts two scrapers' results side by side: Google Maps leads plus each website's tech stack, products plus their stock from another source, companies plus the contacts you already have in your CRM export.
Input fields · API · Use it from Claude, ChatGPT or Cursor (MCP)
Jump to: Fields · Price · How to use · Input · Output · Chain it · AI agents (MCP) · Limits · FAQ
Try it in one click: the input comes pre-filled with the US Treasury's official exchange rates on 2025-12-31, joined by currency with the same table a year earlier: each of the 174 rates gets last year's rate next to it (173 matched, 1 currency was new). 174 rows, about $0.17 (174 × $0.001, plus $0.00005 for the run start). Then pick your own datasets or files.
What data does Join Datasets return?
Each main row with the lookup fields added. From the example:
| Field | Example | Notes |
|---|---|---|
| (your main fields) | "country": "Afghanistan", "exchange_rate": "65.96" | The main row as it is. |
rateAYearEarlier | 70.35 | A lookup field, renamed (exchange_rate -> rateAYearEarlier). |
rateDateAYearEarlier | 2024-12-31 | Another lookup field, renamed. |
joinMatched | true | Whether the row found a lookup row (turn off with Add a matched true/false field). |
Every row has the same columns: when there's no match, the added fields are null. A lookup field whose name the
main row already has is added as lookup_<name>, so your main data is never overwritten. The run's RUN_STATS record
counts the rows read, matched and not matched.
How much does it cost to join two datasets?
You pay per row written: $1.00 per 1,000 rows, plus $0.00005 each time a run starts. Matched or not, it's the same price, and the lookup rows you join with aren't charged.
It's cheaper on paid Apify plans: $0.90 per 1,000 rows on Starter, $0.80 on Scale and $0.70 on Business. The prices on this page are the Free-plan price, so on a paid plan you pay less than the examples show.
- The example: 174 rows × $0.001 = about $0.17, plus the start fee.
- For example: 2,000 Google Maps leads joined with their websites' tech stacks: 2,000 × $0.001 = $2.00; only the 300 leads not in your CRM yet (anti join): $0.30.
- Caps: Max rows per run in the input, and Maximum cost per run in the run options. The run stops cleanly at whichever comes first.
Never charged: main rows an inner or anti join leaves out, lookup rows, and rows too large to write.
How to join two datasets or files
- Open Join Datasets and click Try for free (or Start if you're signed in).
- Pick the rows to enrich in Main data: Apify dataset (or paste a link in Or: main data file URL).
- Pick the rows with the extra fields in Lookup data (a dataset or a file link).
- In Join on, name the field that links them:
url, orwebsite = domainwhen the two call it differently. For websites, set Match values as to Website domain. - Optional: choose Which rows to keep and the Lookup fields to add, then click Start and open the Output tab (export as JSON, CSV or Excel).
Example: exchange rates now and a year earlier
The pre-filled input (two public CSV files, joined by currency, two lookup fields renamed):
{"fileUrl": "https://api.fiscaldata.treasury.gov/services/api/fiscal_service/v1/accounting/od/rates_of_exchange?format=csv&filter=record_date:eq:2025-12-31&page[size]=300","lookupFileUrl": "https://api.fiscaldata.treasury.gov/services/api/fiscal_service/v1/accounting/od/rates_of_exchange?format=csv&filter=record_date:eq:2024-12-31&page[size]=300","joinOn": ["country_currency_desc"],"lookupFields": ["exchange_rate -> rateAYearEarlier", "record_date -> rateDateAYearEarlier"]}
Real output from a local run on 2026-10-03 (the Treasury's bookkeeping columns left out):
[{"country": "Afghanistan", "currency": "Afghani", "exchange_rate": "65.96", "record_date": "2025-12-31","rateAYearEarlier": "70.35", "rateDateAYearEarlier": "2024-12-31", "joinMatched": true},{"country": "Euro Zone", "currency": "Euro", "exchange_rate": "0.851", "record_date": "2025-12-31","rateAYearEarlier": "0.961", "rateDateAYearEarlier": "2024-12-31", "joinMatched": true},{"country": "Curacao", "currency": "Caribbean Guilder", "exchange_rate": "1.78", "record_date": "2025-12-31","rateAYearEarlier": null, "rateDateAYearEarlier": null, "joinMatched": false}]
Ready-to-run examples
Each example opens with the input already filled in. Run it as it is, or change the input first.
- VLOOKUP two CSV files: add a column from another file: Each row of the main file gets the matching row's fields from the lookup file, renamed as you like.
- Find rows that are missing from another file (anti join): Keep only the rows of one table with no match in the other, e.g. leads not yet in your CRM.
Input
| Field | What it does |
|---|---|
| Main data: Apify dataset | The rows to add fields to; every output row is one of these. Or a file in Or: main data file URL. |
| Lookup data (dataset or file URL) | The rows with the fields to add, like VLOOKUP's second table. |
| Join on | field, or main field = lookup field; several lines join on all of them (name and city). Dots reach nested fields. |
| Match values as | Text (ignores case and extra spaces; the default), Exactly as written, or Website domain. |
| Which rows to keep | Every main row (left join, the default), only matches (inner), only non-matches (anti). |
| When a key matches several lookup rows | The first (like VLOOKUP, the default) or one row per match (like SQL). |
| Lookup fields to add (and rename) | field or field -> new name; empty adds every lookup field. |
| Prefix for the added fields | E.g. crm_ gives crm_owner. |
| Add a matched true/false field | Adds joinMatched (on by default). |
| Max rows per run | Cap the rows written. |
Website domain matching reads https://www.acme.com/about, acme.com, ACME.com. and info@acme.com all as
acme.com, so a Maps scraper's website, a crawler's page url and a CRM's email domain line up. Text matching
treats 12 and "12" as the same (a CSV and a dataset join fine), and ignores how an accent is encoded. A row with an
empty key never matches.
A full example, Google Maps leads joined with Tech Stack Detector's results:
{"datasetId": "<Google Maps scraper's dataset>","lookupDatasetId": "<Tech Stack Detector's dataset>","joinOn": ["website = url"],"matchAs": "domain","lookupFields": ["technologies -> techStack", "categories -> techCategories"]}
Output
One dataset row per main row (or per match, with one row per match): the main row's fields, then the added
lookup fields, then joinMatched. The dataset can be downloaded as CSV, JSON, Excel, XML or HTML at any time, or
passed on to the next actor.
Run it after a scraper, or on a schedule
To join each new scrape with a lookup dataset you keep (a CRM export, a list of target domains), chain it:
- Open the scraper or its saved task, go to the Integrations tab and add Apify actor → Join Datasets, triggered when a run succeeds.
- Set
"datasetId": "{{resource.defaultDatasetId}}"(Apify fills in the finished run's dataset), yourlookupDatasetId(a named dataset keeps its name between runs) orlookupFileUrl, andjoinOn.
Or save the input as a task and schedule it, and collect the rows from the API, a webhook, or Make, Zapier and n8n through Apify's integrations.
Can I use Join Datasets from an AI agent (MCP)?
Yes, through Apify's MCP server, from Claude, ChatGPT, Cursor or any other MCP client. Add this to your client's MCP configuration (or let the agent find it with the server's actor search); your client signs you in to Apify:
{"mcpServers": {"apify": {"url": "https://mcp.apify.com?tools=humble-echidna/dataset-join"}}}
To use an Apify API token instead of signing in, add
"headers": {"Authorization": "Bearer <APIFY_TOKEN>"} next to url.
An agent that has run two actors can pass both datasets, e.g.
{"datasetId": "<leads>", "lookupDatasetId": "<stacks>", "joinOn": ["website = url"], "matchAs": "domain"}Who it's for
Anyone who runs more than one scraper on Apify and needs the results in one table: lead lists enriched with technology, contact or company data; product lists with prices from two shops; a fresh scrape checked against the records you already have (anti join).
Why this one?
- VLOOKUP for datasets, without a spreadsheet. Datasets and CSV/TSV, JSON, JSON Lines and Excel files, in any combination, with nested fields and multi-field keys.
- Website domains match out of the box. URLs, bare domains and email addresses line up by domain, the usual key between lead scrapers and website tools.
- Never overwrites your data. Clashing names are added as
lookup_<name>; every row has the same columns. - One flat price, no usage bill: $1.00 per 1,000 rows ($0.90 on Starter), matched or not, about half the price of the alternative for joined rows. Lookup rows are free.
- Fails fast on a typo. If no row has the join field, the run stops before writing (and charging) anything and lists the fields the first row has.
- Polite and safe with files. It identifies itself honestly (User-Agent
HumbleEchidnaApify), follows each site's robots.txt, and only requests public web addresses on the standard ports (80 and 443).
Limits
- Lookup data: up to 200,000 rows, held in memory (at most half of the run's memory; the status says to give the run more memory, keep fewer lookup fields or filter first). Main data: up to 1,000,000 rows, read as a stream.
- Files: up to 200 MB (after decompression), public web addresses on ports 80 and 443; Excel's first sheet only.
- A single output row can be up to 5 MB; larger ones are skipped and counted.
FAQ
Which lookup row is used when several have the same key?
The first one in the lookup data's order, as VLOOKUP does. Choose one row per match to get them all.
Why didn't my rows match?
Check that Join on names each side's field as written (case-sensitive; dots for nested fields). Values that differ in more than case and spaces don't match under Text: a URL and a domain need Website domain. The status counts the main rows without a key value.
Can I find rows that are NOT in another dataset?
Yes: Which rows to keep → only main rows without a match (anti join), e.g. leads not yet in your CRM export.
Is it legal to join datasets and files with it?
It only processes data you choose: your own Apify datasets, and files at addresses you supply, fetched politely (robots.txt honoured, honest User-Agent). Make sure you're allowed to use the files you point it at. The example data is the US Treasury's Fiscal Data API, which is "offered free, without restriction" for commercial and non-commercial use.
Something that used to work now fails. Why?
The run log and the status say what went wrong. Please open an issue with the input you used.
Related actors
| Actor | Use it when |
|---|---|
| Dataset Transformer | You want to filter, dedupe or reshape either side before joining, or the result after, or save it as CSV. |
| Dataset Diff | You want what changed between two runs of the same data, not two different datasets combined. |
| Tech Stack Detector | You need each lead's website technologies to join back onto your leads (it reads their dataset too). |
Feedback and support
Found a bug, or need a join that isn't here? Open an issue on the Issues tab with the input you used.
Versions
Current version: 0.1. See the Changelog tab for what changed in each version.