# Google Sheets Export - Dataset to Sheet (`brightledger/google-sheets-export`) Actor

Send any Apify dataset into your own Google Sheet: append, replace, or add only new rows. Flattens nested data, never runs scraped text as formulas, and pays only for rows written.

- **URL**: https://apify.com/brightledger/google-sheets-export.md
- **Developed by:** [BrightLedger](https://apify.com/brightledger) (community)
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.25 / 1,000 row exports

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 into **your own Google Sheet**: the output of a scraper, a lead list, a price tracker. Append, replace, or add only the rows that are new.

### Why this one

- **Reliable**: small batches with clear error messages that tell you exactly what to fix.
- **Three modes**: append all rows, append only new rows (skip duplicates by a key such as `url`), or replace the tab.
- **Clean columns**: nested data is flattened (`socials.linkedin`), lists become comma-separated text, new fields become new columns at the end without disturbing yours.
- **Safe**: scraped text is never run as a spreadsheet formula.
- **Fair pricing**: you pay only for rows actually written. Skipped duplicates are free.

### Setup option A: Apps Script (5 minutes, no Google Cloud account)

1. Open your Google Sheet. Click **Extensions > Apps Script**.
2. Delete what is there and paste the script below.
3. Change `SECRET` to a long phrase only you know.
4. Click **Deploy > New deployment**, choose **Web app**, set **Execute as: Me** and **Who has access: Anyone**, then **Deploy** and allow access.
5. Copy the **Web app URL** into this Actor's *Apps Script web app URL* field, and the same phrase into *secret phrase*.

The script only writes to the sheet it lives in, and rejects any request without your secret.

**Two things you may see while deploying:**

- **"Google hasn't verified this app."** This is normal for any script you add to your own sheet: the "developer" it names is your own Google account. Click **Advanced**, then **Go to (your project) (unsafe)**, then **Allow**.
- **"Sorry, unable to open the file at this time."** This is a known Google bug when you are signed into several Google accounts in one browser. Open an incognito window, sign in only with the account that owns the sheet, and deploy again.

Keep the web app URL and secret private: together they allow writing to that sheet.

### Setup option B: service account (for Google Cloud users)

1. Create a service account in Google Cloud with the Google Sheets API enabled, and download a JSON key.
2. Share your spreadsheet with the service account's email as **Editor**.
3. Paste the JSON key and the spreadsheet URL into the Actor input.

### Run it after another Actor automatically

In your scraper's **Integrations** tab, add this Actor and set the Dataset field to `{{resource.defaultDatasetId}}`. Every finished run lands in your sheet, and *Append only new rows* keeps it free of duplicates.

### Output

The rows go to your sheet. The run also saves a short summary: rows written, duplicates skipped, number of columns, and whether it stopped at your spending limit.

### Limits

- Google allows 10 million cells per spreadsheet and 50,000 characters per cell; longer text is truncated.
- Very large exports (hundreds of thousands of rows) are faster with option B.

### Receiver script (option A)

```javascript
/**
 * BrightLedger Google Sheets Export - receiver script.
 * Paste this into your sheet: Extensions > Apps Script. Change SECRET, then Deploy > New deployment > Web app,
 * Execute as: Me, Who has access: Anyone. Copy the web app URL into the Actor input together with the same SECRET.
 * The script only writes to the spreadsheet it is attached to.
 */
const SECRET = 'CHANGE-ME-to-a-long-random-phrase';

function doPost(e) {
  let req;
  try {
    req = JSON.parse(e.postData.contents);
  } catch (err) {
    return out({ ok: false, error: 'Request was not valid JSON' });
  }
  if (SECRET.indexOf('CHANGE-ME') === 0) return out({ ok: false, error: 'Set your own SECRET in the script first' });
  if (req.secret !== SECRET) return out({ ok: false, error: 'Secret does not match the one in the script' });

  const lock = LockService.getDocumentLock();
  lock.waitLock(30000);
  try {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    const sheet = ss.getSheetByName(req.sheet) || ss.insertSheet(req.sheet);

    if (req.action === 'read') {
      const lastRow = sheet.getLastRow();
      const lastCol = sheet.getLastColumn();
      if (lastRow === 0 || lastCol === 0) return out({ ok: true, header: [], keys: [] });
      const header = sheet.getRange(1, 1, 1, lastCol).getValues()[0].map(String);
      let keys = [];
      const keyCols = req.keyColumns || [];
      if (keyCols.length && lastRow > 1) {
        const idx = keyCols.map(function (k) { return header.indexOf(k); });
        if (idx.every(function (i) { return i >= 0; })) {
          const data = sheet.getRange(2, 1, lastRow - 1, lastCol).getValues();
          keys = data.map(function (r) { return idx.map(function (i) { return String(r[i]); }).join('␟'); });
        }
      }
      return out({ ok: true, header: header, keys: keys });
    }

    if (req.action === 'write') {
      if (req.clear) sheet.clear();
      const header = req.header || [];
      if (header.length) sheet.getRange(1, 1, 1, header.length).setValues([header]);
      const rows = req.rows || [];
      if (rows.length) {
        const start = Math.max(sheet.getLastRow(), 1) + 1;
        sheet.getRange(start, 1, rows.length, header.length).setValues(rows);
      }
      return out({ ok: true, written: rows.length });
    }
    return out({ ok: false, error: 'Unknown action' });
  } finally {
    lock.releaseLock();
  }
}

function out(obj) {
  return ContentService.createTextOutput(JSON.stringify(obj)).setMimeType(ContentService.MimeType.JSON);
}
```

# Actor input Schema

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

The dataset to export. Run outputs are unnamed, so they may not appear in the list: paste the dataset ID instead (open the run > Storage tab > Dataset ID). When running after another Actor as an integration, use {{resource.defaultDatasetId}}. Without a dataset, the run just explains the setup. With a dataset but no sheet connected, it shows a free preview of the columns and rows it would write.

## `appsScriptUrl` (type: `string`):

Easiest setup, no Google Cloud account. Paste the receiver script from the README into your sheet (Extensions > Apps Script), deploy it as a web app, and paste the URL here.

## `appsScriptSecret` (type: `string`):

The same SECRET you set in the receiver script. Stops anyone else from writing to your sheet.

## `serviceAccountJson` (type: `string`):

For teams using Google Cloud. Paste the service account key, and share the spreadsheet with the service account's email as Editor.

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

The Google Sheet to write to when using a service account.

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

Tab to write to. Created if it does not exist.

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

Append adds rows under existing data, new columns are added at the end. Append only new rows skips rows whose key already exists. Replace clears the tab first.

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

Column names that identify a row, e.g. url or id. Used only by 'Append only new rows'. Nested fields use dots, e.g. socials.linkedin.

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

Leave empty to export every field. Otherwise list the columns in the order you want them.

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

Stop after this many dataset items. 0 means no limit.

## Actor input object example

```json
{
  "sheetName": "Sheet1",
  "mode": "append",
  "maxItems": 0
}
```

# Actor output Schema

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

Rows written, duplicates skipped, columns.

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

```

## MCP server setup

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