# Apify to Google Sheets API – Export to Google Sheets, No OAuth (`tidyrun/google-sheets-export`) Actor

Send any Apify dataset to Google Sheets in one step: append, replace or upsert by key column. No Google Cloud project, no OAuth pop-up and no tokens that expire — just share the sheet with one bot email. Rows that did not change are never charged.

- **URL**: https://apify.com/tidyrun/google-sheets-export.md
- **Developed by:** [Tidyrun](https://apify.com/tidyrun) (community)
- **Categories:** Integrations, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.10 / 1,000 rows

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 Export: Dataset to Sheet

Send any Apify dataset to Google Sheets in one step. No Google Cloud project, no OAuth pop-up, no tokens that expire.

### Quick start (2 minutes)

1. Open your Google Sheet → **Share** → add this email as **Editor**:

   ```
   sheets-sync-bot@sheets-sync-prod-511105.iam.gserviceaccount.com
   ```

   Google may warn that the address can't receive email. That's fine: untick **Notify people** and click **Share anyway**.
2. Paste the sheet's URL in **Google Sheet URL**.
3. Pick the **Dataset to export** (for example, the dataset of your last scraper run).
4. Pick a **Mode** and click **Start**. Not sure? Tick **Dry run** first: it writes nothing and costs only the run start.

When the run finishes, the status message and the **Output** tab show **Open sheet**, a link straight to the tab that was written.

### Export after every scraper run

On your scraper or its task, open **Integrations**, search for **Google Sheets Export**, pick this Actor and set its input:

```json
{
    "spreadsheetId": "https://docs.google.com/spreadsheets/d/1AbC.../edit",
    "sheetName": "Products",
    "mode": "upsert",
    "keyColumns": ["url"],
    "datasetId": "{{resource.defaultDatasetId}}"
}
```

Every time the scraper finishes, its new results land in your sheet. Apify only lets this Actor read the dataset you pass in, nothing else in your account.

### Use it from n8n

Install the Apify community node `@apify/n8n-nodes-apify` (it is already built into n8n Cloud), add an **Apify** node and point it at `tidyrun/google-sheets-export`. A flow people build with it:

**Export every scraper run to a sheet**

`Apify trigger (run succeeded)` → `Get the run's last dataset` → `Apify: tidyrun/google-sheets-export`

Set `datasetId` to the trigger's dataset, `mode` to `upsert` and `keyColumns` to whatever makes a row unique. Share the sheet with the bot email first, or the run stops with a clear message instead of writing nothing.

### Call it from your code

```bash
curl -X POST "https://api.apify.com/v2/acts/tidyrun~google-sheets-export/run-sync?token=YOUR_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"spreadsheetId":"https://docs.google.com/spreadsheets/d/1AbC.../edit","sheetName":"Products","mode":"upsert","keyColumns":["url"],"rows":[{"url":"https://example.com/a","price":19.9}]}'
```

Send `datasetId` instead of `rows` to export a dataset you already have. Add `"dryRun": true` to see exactly what would be written without touching the sheet.

### Modes

| Mode | What it does |
| --- | --- |
| `append` | Adds rows below existing data. |
| `replace` | Backs up the tab, clears it, writes header and rows, then removes the backup. |
| `upsert` | Updates rows whose **Key columns** match, appends new ones, skips unchanged ones. Columns you don't send keep their values. |

Nested objects become dot-path columns (`price.amount`); arrays are written as JSON text. Use **Columns to write** to pick and order columns. Values are stored as-is, so scraped text starting with `=` is never executed as a formula.

### Why this one

| Common problem | What happens here |
| --- | --- |
| "This app is blocked" / tokens expire after a few days | No sign-in at all. Share the sheet with the bot email once; it keeps working. |
| Runs fail on large data with 429 quota errors | Requests are paced and batched under Google's limits and retried automatically. 12,000 rows write in about 20 seconds. |
| Upsert creates duplicates | Keys match ignoring case and extra spaces (`" ABC-1 "` = `"abc-1"`). |
| Paying to rewrite rows that didn't change | Unchanged rows are skipped and **never charged**. |
| A failed replace wiped the sheet | Replace copies your tab first; if anything fails, your data is in the backup tab. |
| "The main function threw an exception" | Plain errors that say what to fix, plus a **dry run** that checks everything before writing. |
| Columns re-sorted or moved | Your column order is kept; new fields are added at the end. |

### Output

```json
{
    "spreadsheetUrl": "https://docs.google.com/spreadsheets/d/1AbC.../edit#gid=0",
    "sheetName": "Products",
    "mode": "upsert",
    "rowsAppended": 120,
    "rowsUpdated": 380,
    "rowsUnchanged": 500,
    "newColumns": ["stock"],
    "stoppedByChargeLimit": false
}
```

### Pricing

| What | Price |
| --- | --- |
| Row written (appended or updated) | $0.10 per 1,000 |
| Run start | $0.002 per run |

Unchanged rows and failed writes are never charged; a dry run costs only the run start. Example: a daily upsert of 2,000 rows where 300 changed costs $0.002 + $0.03. The run stops cleanly at your maximum charge limit.

### FAQ

**My company's Google Workspace won't let me share with an outside email.** Ask your admin to allow sharing with `sheets-sync-bot@sheets-sync-prod-511105.iam.gserviceaccount.com`, or use your own service account (below).

**A teammate gets "already linked to a different Apify account".** The first Apify account that writes to a spreadsheet becomes its owner, so nobody else can write to your sheet just by knowing its ID. Run it from the same Apify account (or organization), or use a copy of the sheet.

**Can I stop access?** Yes. Remove the bot email from **Share** at any time.

### Your own service account (optional)

Want a dedicated quota or can't share with an outside email? Create a service account with Google Sheets API enabled, share the sheet with its `client_email`, and paste its JSON key into **Your own service account key**. Note: Google Cloud organizations created after May 2024 block key creation by default (policy `iam.disableServiceAccountKeyCreation`).

### Limits

- Google allows 20 million cells per spreadsheet and 50,000 characters per cell. Longer cells are truncated and counted in `truncatedCells`; writes that would pass the cell limit are refused before anything is written.
- Upsert reads the whole tab to match keys, so very large tabs take longer.

### Privacy

Rows pass through the Actor run straight to the Google Sheets API. We don't store your data, and the bot only touches spreadsheets you share with it.

Google Sheets is a trademark of Google LLC. This Actor is not affiliated with or endorsed by Google.

# Actor input Schema

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

Step 1: in your sheet click Share and add sheets-sync-bot@sheets-sync-prod-511105.iam.gserviceaccount.com as Editor. Step 2: paste the sheet URL here (or just its ID).

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

Apify dataset to export, e.g. the default dataset of your scraper's run. When connected as an integration, use {{resource.defaultDatasetId}}. Use this or Rows.

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

Append: add rows at the bottom. Replace: clear the tab and write fresh (a backup is kept until it succeeds). Upsert: update rows whose key columns match and add the rest.

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

Column names that identify a row, e.g. \["url"] or \["sku", "store"]. Required for upsert.

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

Created automatically if it does not exist.

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

Check access, tab, columns and how many rows would be appended, updated or skipped. Writes nothing and charges nothing.

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

JSON array of objects to write. Use this or Dataset ID.

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

Optional. Only these columns, in this order. Leave empty to write every field.

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

Optional cap on rows written in this run.

## `normalizeKeys` (type: `boolean`):

Upsert treats " ABC-1 " and "abc-1" as the same key. The sheet keeps its original text.

## `flattenNested` (type: `boolean`):

Turn {"price": {"amount": 5}} into a "price.amount" column.

## `parseValues` (type: `boolean`):

Off (recommended): values are stored as-is, so text starting with = is never run as a formula. On: Sheets parses dates, numbers and formulas.

## `keepBackup` (type: `boolean`):

Replace always copies the tab first, so nothing is lost if a run fails. Turn on to keep the copy after a successful run too.

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

Leave empty to use our shared bot: just share your spreadsheet with the bot email shown in the README. Or paste your own Google service-account JSON key for a dedicated quota. Stored encrypted.

## Actor input object example

```json
{
  "mode": "append",
  "sheetName": "Sheet1",
  "dryRun": false,
  "normalizeKeys": true,
  "flattenNested": true,
  "parseValues": false,
  "keepBackup": false
}
```

# Actor output Schema

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

Link to your sheet and what was written: rows appended, updated and skipped as unchanged.

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

Rows appended, updated and skipped as unchanged, new columns, the spreadsheet URL and whether the run stopped at the maximum charge limit.

# 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("tidyrun/google-sheets-export").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("tidyrun/google-sheets-export").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 tidyrun/google-sheets-export --silent --output-dataset

```

## MCP server setup

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

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/S3UqpgBCf2UoTv5pU/builds/sD3ENgy9MLgMePcbd/openapi.json
