# Dataset Sync to Notion, Slack, Supabase & CRM — with Dedupe (`deadwood_data_solutions/dataset-sync-connector`) Actor

Send any Apify dataset to Notion, Slack, Supabase, HubSpot or any MCP-connected app — writing only the rows that aren't already there. Deterministic deduplication against the destination, explicit field mapping, no LLM and no token cost. Pay only for rows actually written.

- **URL**: https://apify.com/deadwood\_data\_solutions/dataset-sync-connector.md
- **Developed by:** [K O](https://apify.com/deadwood_data_solutions) (community)
- **Categories:** Automation, Integrations, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.50 / 1,000 row written to your destinations

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

## Dataset Sync to Notion, Slack, Supabase & CRM — with Dedupe

Send **any Apify dataset** straight into the app your team actually works in — and write only the rows that aren't already there.

Scrapers leave you with a dataset. Getting that dataset into Notion, Supabase, HubSpot or Slack is the part that stays manual: export a CSV, open the destination, figure out what's already in there, paste the rest. Do it on a schedule and you either re-import duplicates or you hand-check every row.

This Actor does that last mile in one run, and it **deduplicates against the destination itself** — not just against its own history.

### Why this one and not an AI uploader

| | This Actor | LLM-agent uploaders |
|---|---|---|
| Behavior | Deterministic — same input, same writes | Non-deterministic |
| Duplicate rows | Skipped by key, never written | No guarantee |
| Cost | Flat per synced row | Per token, scales with data size |
| Wrong writes to your CRM | Not possible — explicit field map | Possible |

There's no model in the loop. It reads what's at the destination, compares keys, and writes the difference.

### How it works

1. Connect your app (Notion, Slack, Supabase, HubSpot, …) under **Integrations** in Apify Console.
2. Run this Actor in **`list-tools`** mode to see what that connector can do.
3. Run it in **`sync`** mode with the tool name, your dataset ID, and a dedupe key.

Credentials never reach this Actor. It talks to the destination through Apify's MCP proxy, which holds the real tokens and enforces that the Actor can only call the tools it declared.

### Quick start

**Step 1 — discover the destination's tools**

```json
{
  "destination": "<your connector>",
  "mode": "list-tools"
}
```

The dataset comes back with every tool name, description and input schema. Pick the one that creates a record.

**Step 2 — sync**

```json
{
  "destination": "<your connector>",
  "mode": "sync",
  "datasetId": "<dataset from any Actor>",
  "writeTool": "create_page",
  "readTool": "query_database",
  "readToolArgs": { "database_id": "abc123" },
  "writeToolExtraArgs": { "database_id": "abc123" },
  "dedupeKey": "licenseNumber",
  "fieldMap": { "title": "companyName", "phone": "contact.phone" }
}
```

Set `dryRun: true` first — it reports exactly which rows it *would* write, so you can check your field mapping before anything lands in a live database.

### Examples

**Google Maps results → Notion CRM, no repeats**

Point `datasetId` at any Google Maps scraper run, set `dedupeKey` to `placeId`, and schedule it. New places show up in Notion; ones you already have are skipped.

**New leads → Supabase table**

`writeTool: "insert"`, `readTool: "select"`, `readToolArgs: { "table": "leads" }`, `dedupeKey: "email"`.

**Only genuinely new items → Slack**

`writeTool: "send_message"`, `writeToolExtraArgs: { "channel": "#leads" }`, `dedupeKey: "url"`. Because dedupe runs first, the channel only ever sees things nobody has posted before.

### Input

| Field | Required | Description |
|---|---|---|
| `destination` | yes | The MCP connector to write to |
| `mode` | | `sync` (default) or `list-tools` |
| `datasetId` | for sync | Source Apify dataset — any Actor's |
| `writeTool` | for sync | Destination tool called once per new row |
| `dedupeKey` | | Field identifying a record. Dot paths supported. Default `id` |
| `readTool` | | Tool that lists existing records — enables true destination dedupe |
| `readToolArgs` | | JSON args for the read tool |
| `fieldMap` | | Destination arg name → source field path. Empty passes rows through |
| `writeToolExtraArgs` | | Constant args merged into every write |
| `maxItems` | | Cap on rows written per run. Default 1000 |
| `dryRun` | | Plan the sync, write nothing |
| `writeKeyless` | | Also write rows missing the dedupe key. Default off |

### Output

One row per record handled:

```json
{ "synced": true, "key": "roc-123456", "args": { "title": "Acme Builders", "phone": "480-555-0100" } }
```

Failures are recorded rather than silently dropped, and one rejected row never aborts the rest of the run:

```json
{ "synced": false, "key": "roc-123457", "error": "Destination tool \"create_page\" returned an error: ..." }
```

### Dedupe, precisely

- **With `readTool`** — reads current destination state each run. Safe even if records were added by someone else, or the Actor's own history was lost.
- **Without `readTool`** — falls back to remembering keys it wrote in previous runs.
- Keys are trimmed and case-folded, so `"Acme LLC "` and `"acme llc"` are one record.
- Rows duplicated *inside* the source are collapsed too.
- Rows with no key are skipped by default, because writing an unidentifiable row is how duplicates get created.

### Pricing

**$5 per 1,000 rows actually written** to the destination — and nothing else.

There is no per-run fee. Duplicates, skipped rows, dry runs, and scheduled runs over a source that hasn't changed are all free. You pay for records that landed, or you pay nothing.

### FAQ

**Does this Actor see my Notion/Slack token?** No. Apify's MCP proxy holds the credentials and attaches them to outbound calls.

**Which apps work?** Any service exposed as an MCP connector in your Apify account.

**What if I don't know my destination's tool names?** Run `list-tools` mode.

**Can an AI agent call this?** Yes — it's exposed through the Apify MCP server. `list-tools` then `sync` is a natural two-step for an agent.

**What if the destination rejects a row?** It's logged to the dataset with the error, and the run continues.

# Actor input Schema

## `destination` (type: `string`):

The MCP connector to write to. Connect the app (Notion, Slack, Supabase, HubSpot, ...) under Integrations in Apify Console first, then pick it here. Credentials stay in Apify's proxy — this Actor never sees your tokens.

## `mode` (type: `string`):

Use 'list-tools' first to discover what the connected app can do and what its tools are called. Then use 'sync' with the tool name you picked.

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

ID of the Apify dataset to send. Any Actor's dataset works — Google Maps, a job scraper, a lead feed, your own. Required when mode is 'sync'.

## `writeTool` (type: `string`):

Name of the MCP tool that creates a record at the destination, e.g. 'create\_page' for Notion, 'send\_message' for Slack, 'insert' for Supabase. Run mode 'list-tools' if you don't know it. Required when mode is 'sync'.

## `dedupeKey` (type: `string`):

Field in the source rows that identifies a record uniquely, e.g. 'licenseNumber', 'url', 'email', 'id'. Dot paths like 'company.domain' work. Rows whose key already exists at the destination are skipped and never charged.

## `readTool` (type: `string`):

Optional but recommended. Name of the MCP tool that lists what is already at the destination, e.g. 'query\_database' for Notion, 'select' for Supabase. Without it, deduplication falls back to this Actor's own memory of previous runs.

## `readToolArgs` (type: `object`):

JSON arguments the read tool needs, e.g. {"database\_id": "abc123"} for Notion or {"table": "leads"} for Supabase.

## `fieldMap` (type: `object`):

Maps destination argument names to source field paths, e.g. {"title": "companyName", "phone": "contact.phone"}. Leave empty to pass each row through unchanged.

## `writeToolExtraArgs` (type: `object`):

Fixed arguments merged into every write call, e.g. {"database\_id": "abc123"} for Notion or {"channel": "#leads"} for Slack.

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

Stop after this many new rows have been written. Protects against an unexpectedly large source dataset.

## `dryRun` (type: `boolean`):

Compute exactly which rows are new and report them, without writing anything to the destination. Free to run and the safest way to check your field mapping.

## `writeKeyless` (type: `boolean`):

By default rows missing the dedupe key are skipped, because writing an unidentifiable row is how duplicates get created. Turn on only if your source genuinely lacks a stable key.

## Actor input object example

```json
{
  "mode": "sync",
  "dedupeKey": "id",
  "readToolArgs": {},
  "fieldMap": {},
  "writeToolExtraArgs": {},
  "maxItems": 1000,
  "dryRun": false,
  "writeKeyless": false
}
```

# 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 = {
    "readToolArgs": {},
    "fieldMap": {},
    "writeToolExtraArgs": {}
};

// Run the Actor and wait for it to finish
const run = await client.actor("deadwood_data_solutions/dataset-sync-connector").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 = {
    "readToolArgs": {},
    "fieldMap": {},
    "writeToolExtraArgs": {},
}

# Run the Actor and wait for it to finish
run = client.actor("deadwood_data_solutions/dataset-sync-connector").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 '{
  "readToolArgs": {},
  "fieldMap": {},
  "writeToolExtraArgs": {}
}' |
apify call deadwood_data_solutions/dataset-sync-connector --silent --output-dataset

```

## MCP server setup

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

```

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/RKno2UyixXjrF13ff/builds/60SuX2PPDEqMGeFYd/openapi.json
