# Dataset to Google Sheets Export (`oldjard/dataset-to-google-sheets`) Actor

Send any Apify dataset to a Google Sheet. Append, replace, or append only new rows (dedupe by key). Runs after any scraper as an integration. Uses your own Google service account, so no shared quota, and every setup mistake gets a plain-English fix.

- **URL**: https://apify.com/oldjard/dataset-to-google-sheets.md
- **Developed by:** [Joshua White](https://apify.com/oldjard) (community)
- **Categories:** Integrations, Automation, Developer tools
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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

## Dataset to Google Sheets Export

Send any Apify dataset to a Google Sheet: after a scraper finishes, on a schedule, or by hand. Rows are **appended**,
the tab is **replaced**, or only **new rows** are added (deduplicated by a key such as `url`), so a daily run never
writes the same item twice.

It signs in with **your own Google service account**. That means no "reconnect your Google account" prompts, no
quota shared with every other user of a public Google app, and nothing that expires. When something is set up wrong,
the run tells you which click fixes it.

### What you get

- **Three modes:** append, replace the tab, or append only new rows (dedupe on one or more fields).
- **Runs as an integration** after any Actor. Pick this Actor under *Integrations* on your scraper; the finished run's
  dataset is exported automatically.
- **Clean columns.** Nested objects become `address/city`-style columns, the same names Apify's CSV export uses.
  Lists become one readable cell (`a, b, c`), or one column per element, or JSON text. You choose.
- **Columns stay put.** On later runs, data is matched to your header by name, even if you reordered columns in the
  sheet. New fields are added as new columns on the right.
- **Field selection:** export only the fields you name, in your order, or leave some out.
- **Big exports, few requests.** Rows are sent in batches of up to ~2 MB, so 60,000 rows take about a dozen requests.
  Rate limits (HTTP 429) and Google hiccups are retried with backoff for up to about two minutes.
- **Safe with scraped text.** Values are written as plain values, never as formulas, so a scraped `=IMPORTXML(...)`
  stays text. Cells over Google's 50,000-character limit are cut short (and counted in the summary) instead of
  failing the run.
- **Checks before it writes.** Access, sharing and the 10-million-cell limit are checked before anything is
  written. Replace mode clears the tab only once the dataset has been read, and an empty dataset leaves the tab as it is.

### Setup (about 5 minutes, once)

You need a Google service account: a robot Google user that only gets access to the sheets you share with it.
It's free, and Google asks for no billing details to use the Sheets API.

1. Open [console.cloud.google.com](https://console.cloud.google.com) and sign in. In the project picker at the top,
   choose **New project**, name it (for example `apify-sheets`), and click **Create**. Make sure the new project is selected.
2. Open the [Google Sheets API page](https://console.cloud.google.com/apis/library/sheets.googleapis.com) and
   click **Enable**.
3. Go to **IAM & Admin > Service Accounts > Create service account**. Give it a name, click **Create and
   continue**, skip the optional role step, and click **Done**.
4. Click the new service account, open the **Keys** tab, then **Add key > Create new key > JSON > Create**. A
   `.json` file downloads.
5. Open that file in a text editor, copy **all** of it, and paste it into **Google service account key (JSON)**
   in this Actor's input. Apify stores it encrypted, and it never appears in logs.
6. In the file, find `"client_email"` (something like `apify-sheets@your-project.iam.gserviceaccount.com`). Open
   your Google Sheet, click **Share**, paste that email, choose **Editor**, untick *Notify people*, and click **Share**.

Do steps 1–5 once. For each new spreadsheet, just share it with the same email (step 6).

> **Google Workspace (company) accounts:** some organizations block service-account keys. If step 4 says key
> creation is disabled by policy, use a personal Google account for the project. Your sheet can still live
> anywhere, as long as it is shared with the service account's email.

### Input

| Field | What it does |
|---|---|
| **Google Sheet URL** | The sheet's address from your browser. If it ends in `#gid=…`, that tab is used. |
| **Google service account key** | The whole JSON key file (see Setup). |
| **Dataset** | The dataset to export. As an integration: `{{resource.defaultDatasetId}}` (preset). |
| **Mode** | `append` (default), `replace`, or `dedupe` (append only new rows). |
| **Deduplicate by fields** | For dedupe: the field(s) that identify an item, e.g. `url`. |
| **Tab name** | Optional. Created if it doesn't exist (you can turn that off). |
| **Fields to export / Fields to leave out** | Optional column selection and order. A parent name (`address`) selects all its nested columns. |
| **Lists / Nested objects** | How arrays and objects become cells (see above). |
| **Write header row** | On by default; needed for dedupe and for matching columns by name. |
| **Skip first items / Max items** | Export only part of the dataset. |

Example input:

```json
{
  "spreadsheet": "https://docs.google.com/spreadsheets/d/1AbC.../edit#gid=0",
  "serviceAccountKey": "{ \"type\": \"service_account\", ... }",
  "datasetId": "aBcDeFgHiJkLmNoP",
  "mode": "dedupe",
  "dedupeFields": ["url"],
  "fields": ["url", "title", "price"]
}
```

### Run it automatically after a scraper

1. Open your scraper (or one of its saved Tasks) and go to the **Integrations** tab.
2. Choose **Connect Actor or Task**, pick this Actor, and use the **Run succeeded** trigger.
3. Fill in the sheet URL, the key and the mode. **Dataset** is preset to `{{resource.defaultDatasetId}}`, which Apify
   replaces with the finished run's dataset. Keep it as is (if it's empty, type `{{resource.defaultDatasetId}}`).

This Actor runs with Apify's *limited permissions*: it can read only the dataset named in its input, nothing else in
your account.

With mode **dedupe** on `url` (or another ID field), a daily scrape only ever adds what's new.

### Use your sheets from an AI agent

For Claude, Cursor or another MCP client, use the separate **Google Sheets MCP Server** Actor. It shares this
Actor's Google code and works with the same service account.

### Output

Each run saves a summary to the key-value store (`OUTPUT`) and as one dataset item: the sheet link, rows written,
duplicates skipped, new columns, cells cut at 50k characters, and whether the run stopped early. The run's status
message says the same in one line.

### When something is wrong

Each of these ends the run with a message that names the fix:

| Problem | What the message tells you |
|---|---|
| The sheet isn't shared with the service account | The exact email to share with, as Editor |
| Google Sheets API not enabled in the project | The link to the Enable button |
| Wrong or deleted spreadsheet | Where to copy the right URL from |
| An Excel file stored in Drive | Use *File > Save as Google Sheets* |
| You pasted an OAuth client file, an API key, or only the private key | What you pasted, and where to get the right file |
| The key was deleted or the service account removed | How to make a new key |
| A protected tab | Remove the protection or allow the service account |
| The export would pass Google's 10,000,000-cell limit | Checked before writing; use a new spreadsheet or fewer columns |
| A mistyped dataset ID | Where to find the ID (no empty dataset is created) |
| Google's quota stays exhausted after retries | Space out runs, or use one service account per workflow |

### Limits worth knowing

- Google allows **10 million cells per spreadsheet**, counting empty cells in every tab. Long-running append exports
  eventually need a new spreadsheet; replace mode trims unused rows.
- **50,000 characters per cell**. Longer values are cut, and the summary counts them.
- Your service account gets Google's standard quota of **60 write requests per minute**. One run uses a few
  requests per 10,000 rows, so this only matters if many runs write at the same moment.
- New rows go **below the last row that has anything in it**, anywhere in the tab. Keep notes and totals off the
  export tab (or above the table), or they end up in the middle of your data.
- Replace mode clears the tab before writing. If a run fails partway, the tab holds a partial export. Run it again.

### Pricing

Pay per event: you pay per **row written** to your sheet. Header rows and rows skipped as duplicates are free, and a
run that fails before writing anything costs only Apify's tiny start fee. Set a maximum charge on the run, and the
export stops cleanly when it is reached.

### FAQ

**Is my key safe?** The key field is a secret input: Apify encrypts it, and the Actor never logs it. The service
account can only open sheets you explicitly share with it. You can delete the key in Google Cloud at any time.

**Can it write formulas?** No. Everything is written as plain values, on purpose.

**Does it read my other files?** No. It asks Google only for spreadsheet access, and it can see only sheets shared with it.

# Actor input Schema

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

Open the sheet in your browser and copy the address bar (https://docs.google.com/spreadsheets/d/.../edit). The spreadsheet ID alone also works. If the URL ends in #gid=..., that tab is used unless you name one below.

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

Paste the whole JSON key file of a Google service account. It is stored encrypted and never shown in logs. Then share your sheet with the service account's email (the "client_email" in the file) as an Editor. The README's 5-minute setup shows every click.

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

The dataset to export. Pick it from the list or paste its ID. When this runs as an integration after another Actor, this holds {{resource.defaultDatasetId}}, which Apify replaces with the finished run's dataset. Keep it.

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

Append: add rows under what's already in the tab. Replace: clear the tab, then write (an empty dataset leaves the tab untouched). Append only new rows: skip items whose key fields match a row already in the tab, for daily runs without duplicates.

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

For 'Append only new rows': the field(s) that identify an item, e.g. url or id. Several fields form one combined key. Nested fields use a slash: address/city.

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

Which tab to write to. Empty means the tab in the URL's #gid, or else the first tab.

## `createSheet` (type: `boolean`):

Create a tab with that name when the spreadsheet has none.

## `fields` (type: `array`):

Export only these fields, in this order. Naming a parent (address) takes all its nested columns (address/city, address/zip). Empty exports every field. Columns already in the tab keep their place either way.

## `excludeFields` (type: `array`):

Never export these fields (or anything nested under them).

## `writeHeader` (type: `boolean`):

Keep field names in row 1. New fields are added as new columns on the right. Needed for 'Append only new rows' and for matching columns by name on later runs.

## `arrays` (type: `string`):

Join: a list of plain values becomes one cell (a, b, c); lists of objects become JSON text. Columns: one column per element, like Apify's CSV export (tags/0, tags/1). JSON: the whole list as JSON text.

## `arraySeparator` (type: `string`):

Put between values when lists are joined into one cell.

## `objects` (type: `string`):

Columns: one column per nested field (address/city). JSON: the whole object as JSON text in one cell.

## `offset` (type: `integer`):

Start from this item of the dataset.

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

Export at most this many items. Empty means all.

## Actor input object example

```json
{
  "mode": "append",
  "createSheet": true,
  "writeHeader": true,
  "arrays": "join",
  "arraySeparator": ", ",
  "objects": "columns",
  "offset": 0
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `summary` (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("oldjard/dataset-to-google-sheets").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("oldjard/dataset-to-google-sheets").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 oldjard/dataset-to-google-sheets --silent --output-dataset

```

## MCP server setup

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

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/nZ0quWpaDYu5zxpPX/builds/Vgf0V2tAjMDENYbXv/openapi.json
