# Google Sheets Sync: Append & Deduplicate (`neatrow/dataset-to-google-sheets-sync`) Actor

Append Apify dataset items to Google Sheets without rewriting the sheet. Stable columns, de-duplication by key, crash-safe batches and clear fix-it error messages.

- **URL**: https://apify.com/neatrow/dataset-to-google-sheets-sync.md
- **Developed by:** [Neatrow](https://apify.com/neatrow) (community)
- **Categories:** Integrations, Automation
- **Features:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.10 / 1,000 row writtens

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

## Google Sheets Sync: Append & Deduplicate

Send the results of any Apify Actor straight into a Google Sheet. New rows are **added under your existing data**. The sheet is never rewritten, so your formulas, notes and extra columns stay where they are.

Typical uses:

- **Daily scraper → sheet.** Add this Actor as an integration on your scraper. Every run appends only the new results.
- **Price or job tracking.** De-duplicate by `url` or `id`, so the same product or job never shows up twice, even across hundreds of runs.
- **Hand-off to non-technical teammates.** They get a plain spreadsheet that keeps filling itself.

### Why this instead of a full export

| | This Actor | Typical full export |
|---|---|---|
| Appending | Writes only the new rows | Often re-reads and rewrites the whole sheet |
| Re-runs and retries | De-duplicate by any field, so it's safe to run again | Duplicates or overwrites |
| Crashes and retries | Every write targets an exact range and is checked after a restart, so a crash or a retried request never duplicates rows or bills them twice | Can duplicate rows |
| New fields in your data | Added as new columns at the right end. Existing columns never move | Columns can shift |
| Errors | Tells you exactly what to fix (for example "share the sheet with *bot@…* as Editor") | Generic API error |
| Formula injection | Values are written as plain values, so a scraped `=IMPORTXML(...)` never runs | Depends |

### Setup (about 3 minutes, one time)

1. Open [Google Cloud Console](https://console.cloud.google.com/), then create a project, or pick an existing one.
2. Go to **APIs & Services → Library**, search for **Google Sheets API** and click **Enable**.
3. Go to **IAM & Admin → Service accounts → Create service account**. Any name works, and it needs no roles.
4. Open the service account, then **Keys → Add key → Create new key → JSON**. A `.json` file downloads.
5. Open your spreadsheet, click **Share**, and add the service account's email (it looks like `name@project.iam.gserviceaccount.com`) as an **Editor**.
6. Paste the whole content of the `.json` file into **Google service account key** in this Actor. Apify stores it encrypted.

### Try it without setup

Run it with an empty input (or with only `datasetId`) to see a free preview of the rows exactly as they would be written: nested fields flattened, columns in order. Nothing is written and no rows are billed.

### Input

| Field | What it does |
|---|---|
| `spreadsheet` | Sheet URL or ID |
| `sheetName` | Tab name. It's created if missing. Default `Sheet1` |
| `serviceAccountKey` | JSON key from the setup above (secret) |
| `datasetId` | Dataset to sync. As an integration use `{{resource.defaultDatasetId}}` |
| `mode` | `append` (default) or `replace` (clears values but keeps formatting, then writes) |
| `dedupeFields` | For example `["url"]`. Skips items already in the sheet |
| `columns` | Optional. Write only these fields, in this order |
| `maxRows` | Stop after this many rows (0 = no limit) |

Nested objects are flattened to `parent.child` columns. Arrays are written as JSON text.

#### Use it as an integration

On your scraper (Actor or task), open **Integrations → Add → Apify Actor** and pick this Actor. Fill in the spreadsheet and key, and set **Dataset ID** to `{{resource.defaultDatasetId}}`. Each finished run is then synced automatically. Add `dedupeFields` (for example `["url"]`) so repeated results are skipped.

#### Example input

```json
{
  "spreadsheet": "https://docs.google.com/spreadsheets/d/1AbC.../edit",
  "sheetName": "Products",
  "serviceAccountKey": "{ \"type\": \"service_account\", ... }",
  "datasetId": "a1B2c3D4e5F6g7H8i",
  "mode": "append",
  "dedupeFields": ["url"]
}
```

### Output

The run's dataset and `OUTPUT` record hold a summary:

```json
{
  "rowsWritten": 1240,
  "duplicatesSkipped": 85,
  "itemsRead": 1325,
  "columns": 14,
  "stopReason": "done",
  "spreadsheetUrl": "https://docs.google.com/spreadsheets/d/.../edit#gid=0"
}
```

### Pricing

Pay per event: a small fee per run plus a fee per **row actually written**. Skipped duplicates are free. If a run reaches your maximum cost per run, it stops cleanly and tells you. In `append` mode with `dedupeFields` set, run it again to continue: rows already in the sheet are skipped.

### Limits

- Google Sheets holds at most 10 million cells per spreadsheet. A cell holds at most 50,000 characters, so longer values are truncated and marked `…[truncated]`.
- Google rate limits are handled automatically with retries and backoff.
- Don't write to the same tab from two runs at once.
- A run that would push the spreadsheet past 10 million cells stops before writing and tells you.

### FAQ

**"Permission denied".** The sheet isn't shared with the service account email. The error message shows the exact email to add as Editor.

**Can I keep formulas next to the data?** Yes. In `append` mode existing cells are never touched. Only empty rows in the data columns are written, so formula columns to the right of the data stay as they are.

**Does `replace` delete my formatting?** No. It clears values only. It only clears once the run has something to write, so an empty or failed input never empties your sheet.

**I added my own columns (formulas, notes). Are they safe?** Yes. The Actor only writes the columns of the fields it syncs. New fields are added to the right of everything already in use, never on top of your columns.

Found a bug or need a feature? Open an issue on the **Issues** tab. We answer quickly.

# Actor input Schema

## `spreadsheet` (type: `string`):

The Google Sheets URL (or just its ID). Share it with your service account as Editor. Leave this and the key empty to see a free preview of the rows.

## `sheetName` (type: `string`):

Created automatically if it does not exist.

## `serviceAccountKey` (type: `string`):

Paste the whole JSON key file of a Google Cloud service account with the Google Sheets API enabled. Stored encrypted. See the README for a 3-minute setup. Required to write; without it the Actor only shows a preview.

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

The Apify dataset to sync. As an integration on another Actor, set it to {{resource.defaultDatasetId}} so each finished run is synced.

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

append: add new rows under existing data, never touching it. replace: clear the sheet values (formatting kept) and write fresh.

## `dedupeFields` (type: `array`):

Skip items whose values in these fields already exist in the sheet, e.g. \["url"]. Makes re-runs and retries safe. Use dot notation for nested fields, e.g. "product.id".

## `columns` (type: `array`):

Optional. Write only these fields, in this order. Leave empty to write every field (nested objects are flattened as parent.child).

## `maxRows` (type: `integer`):

0 = no limit.

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

Rows per Google Sheets request. Progress is saved after each batch.

## `rows` (type: `array`):

Optional. Items to write instead of a dataset.

## Actor input object example

```json
{
  "sheetName": "Sheet1",
  "mode": "append",
  "maxRows": 0,
  "batchSize": 1000
}
```

# Actor output Schema

## `summary` (type: `string`):

Rows written, duplicates skipped, items read, stop reason and the spreadsheet URL.

## `output` (type: `string`):

No description

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("neatrow/dataset-to-google-sheets-sync").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("neatrow/dataset-to-google-sheets-sync").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 '{}' |
apify call neatrow/dataset-to-google-sheets-sync --silent --output-dataset

```

## MCP server setup

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

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/MGaOhaELToljxFheN/builds/mUlYqVbgoDbO8cHCw/openapi.json
