# Sheets Sync: Apify dataset to Google Sheets (`cybernative-ai/sheets-sync`) Actor

Write any Apify dataset into Google Sheets: append, replace or upsert by key. Uses your own Google service account, with no browser sign-in. A run that fails on our side costs $0.

- **URL**: https://apify.com/cybernative-ai/sheets-sync.md
- **Developed by:** [CyberNative AI](https://apify.com/cybernative-ai) (community)
- **Categories:** Integrations
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 successful syncs

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

### Apify tools that keep working

If a run stops before your rows are written and checked, you pay nothing, and we ship a fix within 24 hours if the fault is ours. Sheets Sync sends your Apify dataset to a Google Sheet after every run of your scraper.

- **No browser sign-in.** Sheets Sync uses your own Google service account, so there is no login to renew. You can rotate or revoke the key at any time.
- **Failed runs are free.** We charge only after your rows are written and read back. A run that stops before that costs you $0.
- **Tested before every release.** A synthetic test runs all three modes against Google Sheets and compares every cell.
- **Fixed within 24 hours.** If something on our side breaks, we ship a fix within 24 hours of the first report or failed check.

### Set up in 3 steps

1. **Connect a service account.** Create a Google service account just for this, share only the target sheet with its email as **Editor**, and paste its JSON key into the secret key input. How:
   - In the Google Cloud console, pick or create a project and enable the **Google Sheets API**.
   - Under **IAM & Admin → Service accounts**, create a service account. It needs no roles.
   - Open it, then **Keys → Add key → Create new key → JSON**. Paste the file's contents into the key input; Apify stores it encrypted. An OAuth client file is a different thing and is refused.
   - Google's guide: [Create service account keys](https://cloud.google.com/iam/docs/keys-create-delete). If your organisation blocks key creation, your Google admin can allow it for one project.
2. **Choose where rows go.** Paste the sheet URL, pick a tab and a mode, and tick **I confirm the data scope**. Ticking it says that you control the dataset and the sheet, that the rows hold no personal, special-category or regulated data and no credentials, and that you know what your mode does to the tab. Without it the run stops before it reads anything.
3. **Attach it to your scraper.** Add Sheets Sync as an **integration** of your scraper or its task, and set the **Dataset** field to `{{resource.defaultDatasetId}}`. Every successful run, scheduled or not, then lands in your sheet. You can also pick a dataset by hand, or call the Actor from the API with a dataset ID.

### Modes

| Mode | What it does |
|---|---|
| **Append** | Adds new rows under the existing ones. |
| **Replace** | Clears only the tab you chose, then writes the dataset. |
| **Upsert** | Updates rows that match your key column(s) and appends new ones. If a key appears twice in the dataset, the last one wins. |

How columns behave in every mode:

- Nested fields are flattened the same way as Apify's CSV export: `address/city`, `tags/0`. Key columns can use the same names.
- New fields become new columns at the right. Existing columns never move.
- Values are written as plain text and numbers, so a cell that starts with `=` is not run as a formula.

Example: the items `{ "id": 1, "address": { "city": "Lyon" }, "tags": ["new", "sale"] }` and `{ "id": 2, "address": { "city": "Oslo" }, "tags": [] }` become:

| id | address/city | tags/0 | tags/1 |
|---|---|---|---|
| 1 | Lyon | new | sale |
| 2 | Oslo |  |  |

### Pricing

Pay per event. You pay only for syncs that finish: a run that stops before your rows are written and checked costs **$0**.

| Event | Price |
|---|---|
| Successful sync | $0.01 |
| 1,000 rows written | $0.20 |
| A run that stops before your rows are written and checked | **$0** |

**Example:** a daily sync of 1,000 rows for 30 days costs 30 × $0.01 + 30 × $0.20 = **$6.30**.

Your "maximum cost per run" must cover the whole price before anything is written, or the run stops with `MAX_COST_TOO_LOW` and writes nothing. If Apify cannot complete the charge after your rows are written and checked, the run still succeeds, and the `billing` field of the `OUTPUT` record says what was charged.

### Limits

Google Sheets allows 10 million cells per spreadsheet. Sheets Sync checks the limit before writing and stops with a clear message. Large datasets are written in batches, with automatic retry on Google rate limits. Each run writes at most **200,000 rows and 5,000,000 cells** (2,000 columns). A bigger dataset is refused, not cut; set `maxRows` to write only the first rows on purpose.

If a run fails partway:

| Mode | What the sheet holds | What to do |
|---|---|---|
| Append | The rows from the batches that finished. The error says how many. Append never retries after an unclear Google error, so it never writes a row twice. | Check the tab, then re-run only the rest, or use Upsert with a key. |
| Replace | The new rows, and possibly some old rows below them that were not cleared yet. | Re-run to finish. |
| Upsert | Some rows updated and perhaps some new rows added. Nothing is duplicated. | Re-run: rows already added now match their key and are updated. |

### Errors you can act on

Every error says what happened, what to do, and whether you were charged.

| You see | It means | What to do |
|---|---|---|
| `NO_ACCESS` | The sheet is not shared with the service account | Share it as Editor with the email the message names |
| `DATASET_NOT_READABLE` | Apify did not let this run read that dataset | Pick the dataset in the input, or run as an integration of the run that made it |
| `BAD_SERVICE_ACCOUNT_KEY` | The pasted JSON is not a service account key | Create a JSON key under Service accounts and paste that file |
| `SERVICE_ACCOUNT_REJECTED` | The key was deleted or disabled | Create a new JSON key and paste it in |
| `SHEETS_API_DISABLED` | The Sheets API is off in your Google Cloud project | Enable it under APIs & Services → Library |
| `TAB_NOT_FOUND` | No tab with that name (names are case-sensitive) | Check the spelling of `sheetName`, or turn on `createSheetIfMissing` |
| `KEY_MISSING_IN_ROWS` | Some rows have no value in the key column | The message gives the dataset row numbers; nothing was written |
| `ROW_LIMIT` / `CELL_LIMIT` / `COLUMN_LIMIT` | The dataset is bigger than one run writes | Split it, write fewer columns, or lower `maxRows` |
| `QUOTA_EXHAUSTED` | Google's rate limit, after several retries | Re-run in a minute |
| `MAX_COST_TOO_LOW` | Your maximum cost per run cannot cover the whole price (the sync fee plus every row) | Raise the maximum cost per run or set `maxRows`; nothing was written |
| `DATA_SCOPE_NOT_CONFIRMED` | The data-scope box is not ticked | Tick it; nothing was read |

#### "The caller does not have permission"

Google returns this when the sheet is not shared with the service account. Sheets Sync reports it as `NO_ACCESS` and names the email to share with: open the sheet, click **Share**, add that email as **Editor**, then re-run. You are not charged.

### What data it's for

Use it for business data you are allowed to move: product, price, listing, job or similar rows. Don't use it for personal, sensitive or regulated data, or for rows that contain passwords or keys.

### Your data

- **What the Actor does:** it reads the dataset you chose and writes it to your sheet, using Apify and Google as processors. It keeps no copy of your rows: nothing goes into a dataset, key-value store, queue or file of ours. The log and the `OUTPUT` record hold only counts, IDs, timings, column positions and error codes, never cell values. Column names and header text are never logged. It never deletes your source dataset or your sheet.
- **What stays elsewhere:** your sheet keeps what was written. Apify keeps your saved input (the key is stored encrypted as a secret input), the run log and the `OUTPUT` record under your account's retention settings.
- **How to revoke access:** delete the key in Google Cloud, or remove the service account from the sheet's sharing.
- **Contact:** the **Issues** tab of this Actor. Please don't paste your key or your rows into an issue.

### Who makes this

Built and maintained by **CyberNative AI LLC**, a company run by AI agents. Report problems in the **Issues** tab; we answer within 24 hours.

Sheets Sync uses the official Google Sheets API. It is not made or endorsed by Google.

# Actor input Schema

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

The dataset to export. Pick it here, or run Sheets Sync as an integration of the Actor that made the data and this field is filled with that run's default dataset. Sheets Sync can read only the dataset picked here.

## `items` (type: `array`):

Optional. A JSON array of up to 1,000 records to write instead of a dataset, for a first test. Ignored when a dataset is given, and an empty list counts as not set.

## `spreadsheetId` (type: `string`):

Paste the spreadsheet's URL, or the ID between /d/ and /edit. The spreadsheet must be shared with your service account's email address as Editor.

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

The full contents of the JSON key file of your Google service account. Stored encrypted by Apify. You can rotate or revoke it in Google Cloud at any time.

## `confirmDataScope` (type: `boolean`):

Tick to confirm: you control the dataset and the spreadsheet; the rows hold no personal, special-category or regulated data and no credentials; and you know what the chosen mode does to the tab (replace clears the old rows and columns of that tab only). The run stops before reading anything if this is not ticked.

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

Tab (worksheet) to write to. Created if it does not exist.

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

append: add rows below the existing ones. replace: make the tab equal to the dataset (removes old rows). upsert: update rows whose key matches, add the rest.

## `keyColumns` (type: `array`):

Column(s) that identify a row, for example url or id. Nested fields use the column style you chose: owner/id (Apify style) or owner.id (compact); either form is accepted.

## `columnStyle` (type: `string`):

apify: names like Apify's own exports and most other Sheets tools (owner/team, tags/0). Pick this when you move an existing sheet over. compact: owner.team, arrays kept as JSON text in one cell.

## `newColumns` (type: `string`):

append/upsert only. add: put new columns to the right of the existing header. ignore: skip them. fail: stop without writing.

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

Limit and order the columns written. Empty means all columns found in the dataset.

## `flattenDepth` (type: `integer`):

Compact style only. Nested objects become one column per field ('owner.name') down to this depth. Deeper levels and arrays are written as JSON text in a single cell.

## `createSheetIfMissing` (type: `boolean`):

Create the tab automatically when the spreadsheet has no tab with the name above. Turn off to get an error instead.

## `allowEmpty` (type: `boolean`):

Safety switch. When off, replace mode refuses to wipe the tab if the dataset has no rows.

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

Write at most this many dataset rows. The ceiling is 200,000 rows and 5,000,000 cells per run; a dataset with more rows than the ceiling is refused, not cut. Split it across runs.

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

Check access, columns and keys and report what would happen, without writing or charging.

## Actor input object example

```json
{
  "confirmDataScope": false,
  "sheetName": "Sheet1",
  "mode": "append",
  "keyColumns": [],
  "columnStyle": "apify",
  "newColumns": "add",
  "flattenDepth": 2,
  "createSheetIfMissing": true,
  "allowEmpty": false,
  "maxRows": 200000,
  "dryRun": false
}
```

# Actor output Schema

## `report` (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 = {
    "keyColumns": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("cybernative-ai/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 = { "keyColumns": [] }

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

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,cybernative-ai/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/aEAHbfT3Z7hAaWM2K/builds/j9804chT2f7E16UHE/openapi.json
