# Google Sheets Import & Export | No OAuth, Free Import (`mikirag3/google-sheets-export`) Actor

Export any Apify dataset or JSON to Google Sheets (append, append only new rows, replace) or import a Google Sheet into a dataset for free. No OAuth, no Google Cloud: public link, Apps Script or service account. Quota-safe retries, exact text, new columns added automatically.

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

## Pricing

$0.20 / 1,000 row writtens

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 Import & Export — Apify datasets to Google Sheets and back

**Export** the results of any Actor (or any JSON) to a Google Sheet, or **import** a Google Sheet into a dataset — **without OAuth and without a Google Cloud project**. Built for scheduled pipelines: it does not fail on Google's write quota, it never wipes your sheet by accident, and it keeps your data exactly as scraped.

- **Import a sheet in 10 seconds, free:** paste the link of a sheet shared as "Anyone with the link" and get one dataset item per row (first row = field names). Private sheets work too, with the same Apps Script or service account used for export. Feed a list of URLs, keywords or products from a sheet into any other Actor.
- **3 export modes:** append all rows · **append only new rows** (skips items whose key is already in the sheet) · replace the tab
- **Never fails on quota:** batched writes, automatic retries with back-off on Google's "too many requests" and temporary errors
- **Exact text:** `00123` keeps its zeros, dates stay as written, `=HYPERLINK(...)` in scraped data is **never executed** (formula-injection safe)
- **Columns that follow your data:** nested JSON flattened to `price.value`, new fields added as new columns at the right, existing column order kept
- **Safe replace:** the tab is cleared only after the first data has been read — an empty or failed source never leaves you with an empty sheet
- **Large exports:** streamed in pages, no size limit on the input dataset; stops cleanly with a clear message before Google's 10 million cells per spreadsheet
- **Free preview:** run it without a Google connection to see exactly how your rows will look

### Quick start (Apps Script, about 2 minutes, once per spreadsheet)

1. Open your Google Sheet → **Extensions → Apps Script**. Delete what's there and paste the script below.
2. Change `SECRET` to a long password of your choice. Save (💾).
3. **Deploy → New deployment →** gear icon **→ Web app**. *Execute as:* **Me**. *Who has access:* **Anyone**. **Deploy** and authorize with your Google account.
4. Copy the **Web app URL** (`https://script.google.com/macros/s/…/exec`).
5. In this Actor: pick the dataset, paste the URL and the same secret, **Start**.

The script only works on the spreadsheet it is attached to and only answers requests carrying your secret. "Anyone" is required so the Actor can call it without logging in as you; the secret is what protects it. To stop access at any time: **Deploy → Manage deployments → Archive**.

<details>
<summary><b>The bridge script (Code.gs) — click to expand and copy</b></summary>

```javascript
/**
 * Bridge for the Apify Actor "Google Sheets Import & Export" (apify.com/mikirag3/google-sheets-export).
 *
 * Setup (about 2 minutes, once per spreadsheet):
 *   1. In your spreadsheet: Extensions > Apps Script. Replace everything with this file.
 *   2. Change SECRET below to a long password of your choice. Save.
 *   3. Deploy > New deployment > Select type: Web app.
 *      Execute as: Me.  Who has access: Anyone.  Deploy, then authorize.
 *   4. Copy the Web app URL. Put it and the same SECRET in the Actor input.
 *
 * The script reads and writes only the spreadsheet it is attached to, and only when a request
 * carries the right SECRET. It stores nothing outside this spreadsheet.
 * If you change this file later, use Deploy > Manage deployments > Edit > New version.
 */
const SECRET = 'change-me-to-a-long-random-password';
const VERSION = 2;

function doGet() {
  return reply({ ok: true, bridge: 'apify-google-sheets-export', version: VERSION });
}

function doPost(e) {
  let req;
  try {
    req = JSON.parse(e.postData.contents);
  } catch (err) {
    return reply({ ok: false, code: 'BAD_REQUEST', error: 'Request body is not valid JSON.' });
  }
  if (!SECRET || SECRET === 'change-me-to-a-long-random-password') {
    return reply({ ok: false, code: 'SECRET_NOT_SET', error: 'Edit SECRET at the top of the script, save, then deploy a new version.' });
  }
  if (req.secret !== SECRET) {
    return reply({ ok: false, code: 'BAD_SECRET', error: 'The secret in the Actor input does not match SECRET in the script.' });
  }
  if (req.action === 'read') {  // reading needs no lock
    try {
      return reply(read(SpreadsheetApp.getActiveSpreadsheet(), req));
    } catch (err) {
      return reply({ ok: false, code: 'SHEETS_ERROR', error: String((err && err.message) || err) });
    }
  }
  const lock = LockService.getScriptLock();
  if (!lock.tryLock(60000)) {
    return reply({ ok: false, code: 'BUSY', error: 'Another export is writing to this spreadsheet. Retry later.' });
  }
  try {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    if (req.action === 'info') return reply(info(ss, req));
    if (req.action === 'prepare') return reply(prepare(ss, req));
    if (req.action === 'append') return reply(append(ss, req));
    return reply({ ok: false, code: 'BAD_REQUEST', error: 'Unknown action: ' + req.action });
  } catch (err) {
    return reply({ ok: false, code: 'SHEETS_ERROR', error: String((err && err.message) || err) });
  } finally {
    lock.releaseLock();
  }
}

function sheetFor(ss, name) {
  return name ? ss.getSheetByName(name) : ss.getSheets()[0];
}

function info(ss, req) {
  const sheets = ss.getSheets();
  let cells = 0;
  sheets.forEach(function (s) { cells += s.getMaxRows() * s.getMaxColumns(); });
  const sh = sheetFor(ss, req.sheet);
  const out = {
    ok: true, version: VERSION, spreadsheetUrl: ss.getUrl(), spreadsheetName: ss.getName(),
    totalCells: cells, sheet: sh ? sh.getName() : req.sheet, exists: !!sh,
    header: [], lastRow: 0, keyValues: null,
  };
  if (!sh) return out;
  out.lastRow = sh.getLastRow();
  const lastCol = sh.getLastColumn();
  if (out.lastRow > 0 && lastCol > 0) out.header = sh.getRange(1, 1, 1, lastCol).getDisplayValues()[0];
  if (req.keyColumn && out.lastRow > 1) {
    const idx = out.header.indexOf(req.keyColumn);
    if (idx >= 0) {
      out.keyValues = sh.getRange(2, idx + 1, out.lastRow - 1, 1).getValues().map(function (r) { return r[0]; });
    }
  }
  return out;
}

function prepare(ss, req) {
  let sh = sheetFor(ss, req.sheet);
  if (!sh) sh = ss.insertSheet(req.sheet);
  if (req.clear) sh.clearContents();
  const header = req.header || [];
  if (header.length) {
    if (sh.getMaxColumns() < header.length) sh.insertColumnsAfter(sh.getMaxColumns(), header.length - sh.getMaxColumns());
    sh.getRange(1, 1, 1, header.length).setValues([header]);
    if (req.freezeHeader && sh.getFrozenRows() === 0) sh.setFrozenRows(1);
  }
  SpreadsheetApp.flush();
  return { ok: true, sheet: sh.getName(), lastRow: sh.getLastRow() };
}

function append(ss, req) {
  const cache = CacheService.getScriptCache();
  const key = req.batchId ? 'batch:' + String(req.batchId).slice(0, 200) : null;
  if (key && cache.get(key)) return { ok: true, written: 0, duplicate: true };
  const sh = sheetFor(ss, req.sheet);
  if (!sh) throw new Error('Sheet not found: ' + req.sheet);
  const rows = req.rows || [];
  if (!rows.length) return { ok: true, written: 0 };
  const width = rows[0].length;
  const start = Math.max(sh.getLastRow(), 1) + 1;
  const missingRows = start + rows.length - 1 - sh.getMaxRows();
  if (missingRows > 0) sh.insertRowsAfter(sh.getMaxRows(), missingRows);
  if (sh.getMaxColumns() < width) sh.insertColumnsAfter(sh.getMaxColumns(), width - sh.getMaxColumns());
  sh.getRange(start, 1, rows.length, width).setValues(rows);
  SpreadsheetApp.flush();
  if (key) cache.put(key, '1', 21600);
  return { ok: true, written: rows.length, firstRow: start };
}

function read(ss, req) {
  let rng;
  let sh;
  if (req.range && String(req.range).indexOf('!') >= 0) {
    rng = ss.getRange(req.range);
    sh = rng.getSheet();
  } else {
    sh = sheetFor(ss, req.sheet);
    if (!sh) throw new Error('Sheet not found: ' + req.sheet);
    rng = req.range ? sh.getRange(req.range) : sh.getDataRange();
  }
  const r0 = rng.getRow();
  const c0 = rng.getColumn();
  const nc = rng.getNumColumns();
  const nr = Math.max(0, Math.min(rng.getNumRows(), sh.getLastRow() - r0 + 1));
  const start = Math.max(0, Number(req.start) || 0);
  const max = Math.min(Math.max(1, Number(req.max) || 5000), 20000);
  const out = { ok: true, sheet: sh.getName(), spreadsheetUrl: ss.getUrl(), spreadsheetName: ss.getName(),
                totalRows: nr, rows: [] };
  if (start >= nr || nc < 1) return out;
  out.rows = sh.getRange(r0 + start, c0, Math.min(max, nr - start), nc).getDisplayValues();
  return out;
}

function reply(obj) {
  return ContentService.createTextOutput(JSON.stringify(obj)).setMimeType(ContentService.MimeType.JSON);
}
```

</details>

### Import a Google Sheet into a dataset

**Public sheet (no setup):** in Google Sheets, *Share → General access → Anyone with the link (Viewer)*. Open the tab you want, copy the URL from the browser, then run:

```json
{
  "operation": "import",
  "authMethod": "public",
  "spreadsheet": "https://docs.google.com/spreadsheets/d/…/edit#gid=0"
}
```

**Private sheet:** use the same Apps Script bridge (version 2 of the script below; if you installed version 1, paste the new one and *Deploy → Manage deployments → Edit → New version*) or a service account with *Viewer* access, and set `"operation": "import"` plus `"sheetName"` if it is not the first tab.

Each non-empty row becomes an item, keyed by the header row (empty names become `column3`, repeated ones `name_2`). Cells come as text **exactly as shown in the sheet** (`00123` keeps its zeros); turn on *Convert numbers and TRUE/FALSE* to get numbers and booleans. Optional: a range (`A1:F500`), only some columns, skip/limit rows.

Typical use: keep your input list (start URLs, search terms, product IDs, companies to watch) in a sheet your team edits, import it at the start of a scheduled workflow and pass the dataset to the next Actor.

### Alternative: service account (teams, automation)

1. In Google Cloud Console create a project, enable the **Google Sheets API**, create a **service account** and download a **JSON key**.
2. Share your spreadsheet with the service account's `client_email` as **Editor**.
3. In this Actor choose *Service account key*, paste the whole JSON key (stored encrypted) and the spreadsheet URL. (For import only, *Viewer* access is enough.)

### Send every run of another Actor to Sheets automatically

On the Actor whose results you want in Sheets: **Integrations → Connect Actor or task → Google Sheets Import & Export**, and set the input to:

```json
{
  "datasetId": "{{resource.defaultDatasetId}}",
  "authMethod": "appsScript",
  "appsScriptUrl": "https://script.google.com/macros/s/…/exec",
  "appsScriptSecret": "your-secret",
  "mode": "append-new",
  "dedupeKey": "url"
}
```

Each time that Actor finishes, only the new items land in your sheet. Pair it with a schedule for a self-updating spreadsheet (price trackers, job lists, lead lists, monitoring).

### Use it from the API

```bash
curl -X POST "https://api.apify.com/v2/acts/mikirag3~google-sheets-export/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"rawData":[{"name":"Ada","score":10}],"appsScriptUrl":"https://script.google.com/macros/s/…/exec","appsScriptSecret":"your-secret"}'
```

### Input

| Field | What it does |
|---|---|
| Operation | `export` (default) or `import`. |
| Dataset to export | Any dataset you can access (e.g. another run's results). Or paste **raw JSON** instead. |
| How to connect | Apps Script web app (recommended) · service-account key · public link (import only). |
| Tab name | Export: created if missing. Import: tab to read. Empty = first tab. |
| Mode | `append` · `append-new` (with **Dedupe key**, e.g. `url`, nested `product.sku`) · `replace`. |
| Columns | Optional: only these fields, in this order. |
| Arrays | `join` (`a, b, c` in one cell) · `json` · `columns` (`tags.0`, `tags.1`…). |
| Let Google convert text | Off by default (exact text). On = like typing in the cell: numbers, dates and formulas are interpreted. |
| Range · First row is the header · Convert numbers | Import options (see above). |
| Skip first / Maximum items | Export or import a slice. |

### Output

**Import:** the dataset holds one item per row; the summary (`rowsImported`, `emptyRowsSkipped`, `columns`, sheet and spreadsheet) is in the key-value store as `OUTPUT`.

**Export:** the run summary (also in the key-value store as `OUTPUT`):

```json
{
  "status": "ok",
  "rowsWritten": 1250,
  "skippedDuplicates": 318,
  "mode": "append-new",
  "sheet": "Leads",
  "spreadsheetName": "My leads",
  "spreadsheetUrl": "https://docs.google.com/spreadsheets/d/…",
  "columns": ["name", "url", "price.value"]
}
```

### Pricing

**Export: $0.20 per 1,000 rows written** — no start fee, preview runs free, duplicates skipped in `append-new` mode are free. **Import: free**, up to 5,000 rows per run (use *Skip first items* to read the next 5,000 in another run). Set a *maximum cost per run* in the run options: the Actor stops cleanly at that limit and never writes rows it cannot charge.

### Limits (Google's, not ours)

- 10 million cells per spreadsheet (all tabs together) — the Actor stops before exceeding it and tells you where.
- 50,000 characters per cell — longer texts are truncated with `…[truncated]`.
- Apps Script: up to 6 minutes per call; the Actor sends batches of up to 5,000 rows, so any size of export works.

### FAQ

**Why an Apps Script instead of "Connect Google account"?** The bridge runs inside your own spreadsheet: no Google Cloud project, no OAuth token that expires or gets revoked, and you can switch it off any time from *Manage deployments*.

**Can I read a sheet without sharing it publicly?** Yes: use the Apps Script bridge (it reads only the spreadsheet it is attached to) or a service account with Viewer access.

**Is my data stored anywhere?** No. Exported rows go from the dataset straight to your spreadsheet; imported rows go only to your run's dataset. Secrets and keys are stored encrypted by Apify.

**A run failed — what now?** The status message says exactly what to fix (wrong secret, deployment not set to "Anyone", sheet not shared with the service account…). Quota and temporary Google errors are retried automatically, and with the Apps Script method a retried batch is not written twice.

**Found a bug or need a feature?** Open an issue on the Actor page.

# Actor input Schema

## `operation` (type: `string`):

Export writes rows into a sheet (paid per row written). Import reads a tab or range into this run's dataset, one item per row, using the first row as field names (free, up to 5,000 rows per run).

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

The dataset to write into the sheet, e.g. the results of another Actor run. In an integration (Actor → this Actor), use {{resource.defaultDatasetId}}.

## `rawData` (type: `array`):

Instead of a dataset: a JSON array of objects (one row each) or of arrays (cells in order). Ignored when a dataset is chosen.

## `authMethod` (type: `string`):

Apps Script: paste the free bridge script from the README into your sheet and deploy it as a web app. Service account: a Google Cloud service-account JSON key, with the sheet shared to its email. Public link: just paste the sheet's URL below (reading only).

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

Looks like https://script.google.com/macros/s/…/exec (Deploy > New deployment > Web app). The data goes to the spreadsheet the script is attached to.

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

The same password you wrote in the script (const SECRET = '…'). Stored encrypted.

## `serviceAccountKey` (type: `object`):

Only for the service-account method: the whole JSON key file. Share the spreadsheet with the key's client_email as Editor. Stored encrypted.

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

For the service-account and public-link methods: the full Google Sheets URL (or its ID). For a public link, copy the URL while the tab you want is open (it ends with #gid=…). The Apps Script method always uses its own spreadsheet.

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

Export: tab to write to, created if missing. Import: tab to read. Empty = the first tab. (Public links pick the tab from the URL's #gid=.)

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

Replace clears the tab only after the first data has been read, so an empty or failed source never wipes your sheet.

## `dedupeKey` (type: `string`):

For 'Append only new rows': the field that identifies an item, e.g. url or id (nested fields with dots: product.sku).

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

Export: write only these fields, in this order (nested fields with dots: price.value); empty = all fields, new fields become new columns at the right. Import: keep only these header names.

## `arrayMode` (type: `string`):

How list fields are written.

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

Off (recommended): text stays exactly as scraped — 00123 keeps its zeros, '=…' is never run as a formula. Numbers and booleans are always written as numbers and booleans.

## `range` (type: `string`):

A1 range such as A1:F500 or A:D (Apps Script and service account also accept Tab!A1:F500). Empty = all data on the tab.

## `hasHeader` (type: `boolean`):

On: the first non-empty row gives the field names (empty names become column3, repeated names get \_2). Off: fields are column1, column2…

## `convertValues` (type: `boolean`):

Off: every cell comes as text exactly as shown in the sheet. On: plain numbers (12, 3.5) become numbers and TRUE/FALSE booleans; codes with leading zeros like 00123 stay text.

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

Export: items of the source to skip. Import: data rows to skip (to continue after a previous run).

## `limit` (type: `integer`):

Export: empty = all items. Import: empty = up to 5,000 rows.

## Actor input object example

```json
{
  "operation": "export",
  "rawData": [
    {
      "title": "Wireless mouse",
      "price": {
        "value": 19.99,
        "currency": "EUR"
      },
      "sku": "00123",
      "tags": [
        "office",
        "usb"
      ],
      "url": "https://example.com/p/1"
    },
    {
      "title": "Mechanical keyboard",
      "price": {
        "value": 89,
        "currency": "EUR"
      },
      "sku": "00456",
      "tags": [
        "office"
      ],
      "url": "https://example.com/p/2"
    }
  ],
  "authMethod": "appsScript",
  "mode": "append",
  "arrayMode": "join",
  "parseValues": false,
  "hasHeader": true,
  "convertValues": false,
  "offset": 0
}
```

# Actor output Schema

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

Export: rows written, duplicates skipped, target spreadsheet and tab. Import: rows imported, columns, source sheet.

## `rows` (type: `string`):

Import: one item per sheet row. Export: the run summary, or in preview mode the first rows as they would be written.

# 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 = {
    "rawData": [
        {
            "title": "Wireless mouse",
            "price": {
                "value": 19.99,
                "currency": "EUR"
            },
            "sku": "00123",
            "tags": [
                "office",
                "usb"
            ],
            "url": "https://example.com/p/1"
        },
        {
            "title": "Mechanical keyboard",
            "price": {
                "value": 89,
                "currency": "EUR"
            },
            "sku": "00456",
            "tags": [
                "office"
            ],
            "url": "https://example.com/p/2"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("mikirag3/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 = { "rawData": [
        {
            "title": "Wireless mouse",
            "price": {
                "value": 19.99,
                "currency": "EUR",
            },
            "sku": "00123",
            "tags": [
                "office",
                "usb",
            ],
            "url": "https://example.com/p/1",
        },
        {
            "title": "Mechanical keyboard",
            "price": {
                "value": 89,
                "currency": "EUR",
            },
            "sku": "00456",
            "tags": ["office"],
            "url": "https://example.com/p/2",
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("mikirag3/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 '{
  "rawData": [
    {
      "title": "Wireless mouse",
      "price": {
        "value": 19.99,
        "currency": "EUR"
      },
      "sku": "00123",
      "tags": [
        "office",
        "usb"
      ],
      "url": "https://example.com/p/1"
    },
    {
      "title": "Mechanical keyboard",
      "price": {
        "value": 89,
        "currency": "EUR"
      },
      "sku": "00456",
      "tags": [
        "office"
      ],
      "url": "https://example.com/p/2"
    }
  ]
}' |
apify call mikirag3/google-sheets-export --silent --output-dataset

```

## MCP server setup

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