# Dataset to Google Sheets — No OAuth (`duffy1990/dataset-to-google-sheets`) Actor

Append or replace rows in your Google Sheet from any Apify dataset. Share the sheet once with the Actor's e-mail; no Google login, no tokens that expire.

- **URL**: https://apify.com/duffy1990/dataset-to-google-sheets.md
- **Developed by:** [Duffy Radmilović](https://apify.com/duffy1990) (community)
- **Categories:** Integrations, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 sheet updateds

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 — No OAuth

Send any Apify dataset into your Google Sheet: append new rows, skip duplicates, or replace the tab.
There's **no Google login and no OAuth token**. Share your sheet once with the Actor's e-mail and it keeps
working. No token to expire, no `invalid_grant`, no reconnecting accounts.

### Why this exists

The usual way to get scraped data into Google Sheets is to connect your Google account with OAuth.
Those tokens expire, get revoked, or get tied to the wrong account, and scheduled runs start failing.

This Actor uses a Google **service account** instead. You give it access the same way you'd give a colleague
access: **Share → add an e-mail → Editor**. It can only touch spreadsheets you shared with it.

### Setup (one minute, once per spreadsheet)

1. Create or open a Google Sheet.
2. Click **Share** and add this e-mail as **Editor**:

   ```
   sheets-bot@helena-sheets.iam.gserviceaccount.com
   ```

   If Google says the address has no Google account, share anyway. That's normal for service accounts.
3. Copy the sheet link into the **Google Sheet link** field and pick a dataset.

If you forget step 2, the run tells you exactly that, with the e-mail to add. It charges nothing
beyond Apify's minimal start fee.

### Use it after another Actor (the main use case)

Run a scraper on a schedule and have every run land in the same sheet automatically:

1. Open the scraper (or its task) in Apify Console → **Integrations** → connect an Actor → choose
   **Dataset to Google Sheets — No OAuth**.

2. In the integration input set:

   ```json
   {
     "spreadsheetUrl": "https://docs.google.com/spreadsheets/d/YOUR_SHEET_ID/edit",
     "sheetName": "Results",
     "mode": "append",
     "dedupeBy": "url",
     "datasetId": "{{resource.defaultDatasetId}}"
   }
   ```

3. Done. Each successful scraper run appends only rows whose `url` isn't in the tab yet.

**Important:** the dataset has to arrive through the `datasetId` field. Apify only grants this Actor read
access to datasets passed that way.

Tested end to end: a scraper run followed by this Actor wrote 57 rows. The next identical run wrote
0 and skipped 57 duplicates.

### Input

| field | default | meaning |
|---|---|---|
| `spreadsheetUrl` | *(required)* | Link of the Google Sheet, or its ID. |
| `sheetName` | `Sheet1` | Tab to write into. Created if missing. |
| `datasetId` | — | Dataset to copy. In integrations use `{{resource.defaultDatasetId}}`. |
| `rows` | — | Alternative to a dataset: a JSON array of objects. |
| `mode` | `append` | `append` adds rows below existing ones. `replace` clears the tab first. |
| `dedupeBy` | — | Field name. In `append` mode, rows whose value is already in that column are skipped. |
| `columns` | all | Which fields to write, in this order. Nested fields use dots: `price.amount`. |
| `maxRows` | `0` | Safety brake. `0` = no limit. |

#### How data is laid out

- Nested objects are flattened: `{"price": {"amount": 7}}` → column `price.amount`.
- Lists of simple values become one cell: `["a","b"]` → `a, b`. Lists of objects become a JSON string.
- The header row only ever **grows**. Existing columns keep their position; new fields get new columns on
  the right. Your formulas and formatting to the left stay where they are.
- Values are written **as they are** (`RAW`). Numbers stay numbers and text stays text. A product code like
  `00123` isn't turned into `123`, and a date-like string isn't reinterpreted.

### Output

One row per run in the default dataset:

```json
{
  "status": "OK",
  "spreadsheetId": "17gw...nxU",
  "sheetName": "Shopify",
  "mode": "append",
  "rowsIn": 57,
  "rowsWritten": 0,
  "rowsSkippedDuplicate": 57,
  "columns": ["shopDomain", "productId", "variantId", "title", "price"],
  "sheetUrl": "https://docs.google.com/spreadsheets/d/17gw...nxU/edit#gid=...",
  "message": "Wrote 0 rows to \"Shopify\", skipped 57 duplicates.",
  "finishedAt": "2026-09-24T19:16:10.000Z"
}
```

Problems come back as a row with a clear `status`, not just a log line:

| status | meaning |
|---|---|
| `NOT_SHARED` | The sheet isn't shared with the Actor's e-mail. The message says which e-mail to add. |
| `SPREADSHEET_NOT_FOUND` | The link doesn't point to an existing sheet. |
| `DATASET_NOT_FOUND` | The dataset doesn't exist. |
| `DATASET_NOT_ACCESSIBLE` | The dataset wasn't passed through the `datasetId` field (see above). |
| `EMPTY_DATASET` | Nothing to write. Nothing charged. |
| `BAD_INPUT` | Missing or invalid input. The message says which field. |
| `TOO_LARGE` | Writing would pass Google's 10 million cells per spreadsheet. Checked **before** writing. |
| `RATE_LIMITED` | Google kept refusing after several retries with back-off. |
| `CHARGE_LIMIT_REACHED` | Your maximum cost per run is too low to write anything. |

### Pricing

Pay per event:

| event | price |
|---|---|
| Sheet updated (once per successful run) | $0.005 |
| Rows written, per started 1,000 rows | $0.02 |

- 500 new rows a day → $0.025 a day, about **$0.75 a month**.
- One-off export of 12,000 rows → 12 × $0.02 + $0.005 = **$0.245**.
- A scheduled run with **no new rows** (all duplicates) still costs $0.005, because the sheet was checked.
- Failed runs (not shared, bad input, empty dataset) are **not** charged.

If you set a maximum cost per run, the Actor writes only as many rows as that limit covers, then says so
in the result.

### Limitations — please read

- **It can't create spreadsheets.** Service accounts have no Drive storage of their own, so the Actor
  only writes into a sheet you created and shared.
- **Google's limit is 10 million cells per spreadsheet**, counting every tab. For very large exports use a
  new spreadsheet or pick fewer `columns`.
- **One cell holds at most 50,000 characters.** Longer values are cut off with `…`.
- **Google allows about 60 write requests per minute** for this service account. The Actor writes up to
  5,000 rows per request and retries with back-off, so normal use never gets close. Very heavy parallel use
  may be slowed down.
- **`dedupeBy` checks one column.** It compares values as text.
- **`replace` clears values, not formatting.**

### Privacy

- The Actor can open **only spreadsheets you explicitly shared** with its e-mail. Share a sheet made for this
  purpose, not your whole Drive.
- It reads the spreadsheet's tab names and sizes, plus the header row and the `dedupeBy` column of the tab it
  writes to. It reads no other cell contents.
- Remove access at any time: **Share** → remove the e-mail.
- The Actor stores no copy of your data outside your Apify run.

### FAQ

**Why not just use Apify's Google Drive integration?**
That uploads a file (CSV/JSON) into a Drive folder. This Actor writes rows into a live Google Sheet,
appends, and skips duplicates.

**Can I write several scrapers into one spreadsheet?**
Yes, use a different `sheetName` for each.

**Does it work with shared drives / Google Workspace?**
Yes, if your organisation allows sharing with outside e-mails. Some companies block it. Then the
run returns `NOT_SHARED`.

**What if Google is down or slow?**
Requests are retried with exponential back-off. Rows already written stay written and are reported in the result.

# Actor input Schema

## `spreadsheetUrl` (type: `string`):

Link of your Google Sheet (or its ID). Before the first run, open the sheet, click Share and add the Actor's e-mail as Editor — the e-mail is in the README. The Actor never creates spreadsheets; it only writes into one you shared.

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

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

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

Apify dataset to copy. When this Actor runs as an integration after another Actor, set this field to {{resource.defaultDatasetId}} in the integration input.

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

Optional: a JSON array of objects to write instead of a dataset.

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

append adds rows below existing ones; replace clears the tab first.

## `dedupeBy` (type: `string`):

Field name, e.g. url. In append mode, a row is skipped when this value is already in the tab.

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

Which fields to write and in which order. Empty = all fields. Nested fields use dots, e.g. price.amount.

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

Safety brake. 0 = no limit.

## `payload` (type: `object`):

Filled in automatically when this Actor runs as an integration. Leave empty.

## Actor input object example

```json
{
  "spreadsheetUrl": "https://docs.google.com/spreadsheets/d/PASTE-YOUR-SHEET-ID/edit",
  "sheetName": "Sheet1",
  "rows": [
    {
      "url": "https://example.com/a",
      "price": 10
    },
    {
      "url": "https://example.com/b",
      "price": 12
    }
  ],
  "mode": "append",
  "maxRows": 0
}
```

# Actor output Schema

## `result` (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 = {
    "spreadsheetUrl": "https://docs.google.com/spreadsheets/d/PASTE-YOUR-SHEET-ID/edit",
    "rows": [
        {
            "url": "https://example.com/a",
            "price": 10
        },
        {
            "url": "https://example.com/b",
            "price": 12
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("duffy1990/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 = {
    "spreadsheetUrl": "https://docs.google.com/spreadsheets/d/PASTE-YOUR-SHEET-ID/edit",
    "rows": [
        {
            "url": "https://example.com/a",
            "price": 10,
        },
        {
            "url": "https://example.com/b",
            "price": 12,
        },
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("duffy1990/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 '{
  "spreadsheetUrl": "https://docs.google.com/spreadsheets/d/PASTE-YOUR-SHEET-ID/edit",
  "rows": [
    {
      "url": "https://example.com/a",
      "price": 10
    },
    {
      "url": "https://example.com/b",
      "price": 12
    }
  ]
}' |
apify call duffy1990/dataset-to-google-sheets --silent --output-dataset

```

## MCP server setup

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