# Google Sheets Import & Export (Service Account) (`bridgeworks/sheets-bridge`) Actor

Reliable Google Sheets export/import for Apify datasets. Service-account auth: no OAuth flow, no expiring tokens. Replace, append, or upsert rows; import sheet ranges as typed datasets.

- **URL**: https://apify.com/bridgeworks/sheets-bridge.md
- **Developed by:** [Bridgeworks](https://apify.com/bridgeworks) (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

from $0.50 / 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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Google Sheets Import & Export: service account, nothing expires

Export any Apify dataset to Google Sheets, or import a sheet into a dataset. Authentication is a **Google service account**: you paste a JSON key once, share your sheet with one email address, and it works forever. No OAuth consent screens, no tokens that expire mid-schedule, no re-authorizing after a week.

If your current Sheets integration keeps failing with cryptic auth errors, that is exactly the problem this Actor removes. OAuth-based Sheets connectors break when tokens expire or Google tightens app verification. A service account is a machine identity: it cannot expire that way, and every error this Actor can hit comes back as a plain-English message telling you the fix.

![An exported dataset in Google Sheets: typed values, booleans, timestamps, clean headers](https://bridgeworks-assets.vercel.app/sheets-bridge-sheet.png)

### What it does

**Export (dataset → sheet)**

- Reads any Apify dataset by ID, or the default dataset of the run it is attached to (works as a run integration: scrape → auto-export).
- Three write modes:
  - **replace**: write everything fresh. The new data is staged into a temp tab and atomically swapped into place, so even a mid-write failure can never leave you with a cleared sheet and no data. Tradeoff, stated plainly: the swap gives the tab a new identity, so formulas in OTHER tabs that reference this tab (`='My Tab'!A1`) break with `#REF!` after a replace, and links using the tab's gid change. If other tabs reference your data, use append or upsert, which edit the tab in place and keep every reference intact.
  - **append**: add rows under existing data; new columns get merged into the header
  - **upsert**: match rows on a key column (`id`, `url`, ...); update changed rows in place, append new ones, skip identical ones. Comparison is type-aware (booleans, numbers, dates), so an identical scheduled re-run really is a no-op, and columns you maintain by hand in the sheet (a `notes` column, say) are never touched or blanked by an update.
- Flattens nested JSON to dot-notation columns (`user.address.city`), joins primitive arrays (`a, b, c`), JSON-encodes structured arrays.
- Creates the tab automatically if it does not exist.
- Handles 100K+ rows in both directions: writes are batched and paced under Google's per-user write quota, reads are paged, and 429/5xx get exponential-backoff retries.
- Formula-injection safe: scraped cells starting with `=`, `+` or `@` are stored as literal text, never executed as formulas in your sheet.
- Truncates cells that exceed Google's 50,000-character limit instead of failing the whole run.

**Import (sheet → dataset)**

- Reads a whole tab or an A1 range (`A2:F500`), paged, so very large sheets import without memory or response-size failures.
- First row as header (dataset items keyed by column names) or positional `column_1..n`.
- Values arrive **typed**: numbers stay numbers, booleans stay booleans, empty cells become `null`. Or switch to formatted strings if you want what the sheet displays.

### Quickstart (export)

1. Create a service account and paste its JSON key (3-step setup below, ~2 minutes, one time ever).
2. Share your spreadsheet with the service account's email as **Editor**.
3. Fill the input: spreadsheet URL, tab name, dataset ID (or leave empty and attach the Actor to another Actor's run).
4. Run. Your data is in the sheet.

![The input form: paste the service-account key once, point at a spreadsheet, pick a mode](https://bridgeworks-assets.vercel.app/sheets-bridge-input.png)

### Service account setup (one time, ~2 minutes)

1. **Enable the API.** Go to [console.cloud.google.com](https://console.cloud.google.com), create or pick a project, and enable the **Google Sheets API** ([direct link](https://console.cloud.google.com/apis/library/sheets.googleapis.com)).
2. **Create the key.** IAM & Admin → Service Accounts → *Create service account* (any name, no roles needed) → open it → *Keys* → *Add key* → *Create new key* → **JSON**. A `.json` file downloads.
3. **Paste and share.** Paste the entire file into this Actor's *Service account JSON key* field (it is stored encrypted and never logged). Open the downloaded file, copy the `client_email` value (looks like `something@yourproject.iam.gserviceaccount.com`), and share your spreadsheet with that email as **Editor**, same as sharing with a person.

That's it. The key does not expire. You never do this again.

### Errors that tell you what to do

Every failure path returns a specific, actionable message instead of a stack trace:

| Situation | What you'll see |
| --- | --- |
| Sheet not shared with the service account | "Share the sheet with `<your-sa-email>` as Editor", with the exact email filled in |
| Sheets API not enabled on your project | A direct enable link with your project ID filled in |
| Wrong spreadsheet ID / URL | Where exactly in the URL the ID lives |
| Pasted an OAuth client secret instead of a service-account key | Which file you grabbed by mistake and where the right one is |
| Truncated/corrupted private key | How to mint a fresh key in two clicks |
| Google quota hit | Retried automatically with backoff; only surfaces if it truly persists |
| Bad tab name or A1 range | Which of the two it is, and that tab names are case-sensitive |

### Pricing (pay per event)

| Event | Price |
| --- | --- |
| Actor start | $0.01 per run |
| Rows written or read | $0.50 per 1,000 rows, charged per exact row ($0.0005) |

A daily scrape exporting 500 rows costs about $0.26/day, and you are charged for exact rows, never rounded up to a block. Upsert runs charge only for rows actually written: unchanged rows are free. No subscription, no rental fee; you pay only when it runs. Pay-per-event also makes this Actor callable by AI agents via Apify's MCP server and agentic payments.

If your run has a spending limit (`maxTotalChargeUsd`) that does not cover the whole export, the Actor charges what the limit allows, processes exactly that many rows, and tells you how many were skipped. It never does uncharged work and never charges for undone work.

### Use as a run integration

Attach this Actor as an integration on any scraper: when the scraper finishes, this Actor runs with the scraper's default dataset and pushes the results into your sheet. Leave *Dataset ID* empty and the run's dataset is used automatically. With **upsert** mode and a key column, a scheduled scraper maintains a living sheet: new items appended, changed items updated, untouched rows left alone.

### Input reference

See the input schema in the console for full descriptions. The short version:

| Field | Notes |
| --- | --- |
| `mode` | `export` (default) or `import` |
| `serviceAccountJson` | The whole JSON key file, pasted as-is. Secret field, encrypted at rest. |
| `spreadsheetId` | Spreadsheet URL or bare ID |
| `sheetName` | Tab name; auto-created on export. Default `Sheet1` |
| `datasetId` | Export source; empty = the run's default dataset |
| `exportMode` | `replace` / `append` / `upsert` |
| `keyColumn` | Required for upsert, e.g. `id` or `user.email` (dot notation for nested fields) |
| `range` | Import only; A1 range, empty = whole tab |
| `headerRow` | Import only; first row provides field names (default on) |
| `valueRender` | Import only; `typed` (default) or `formatted` |
| `testMode` | Runs the whole pipeline against an in-memory fake, so you can try the Actor without any Google setup |

### FAQ

**Why a service account instead of OAuth?**
OAuth was designed for apps acting on behalf of a user, which drags in consent screens, app verification, and refresh tokens that expire or get revoked. For an automation writing to a sheet on a schedule, a service account is the mechanism Google built for exactly this. One shared email, one pasted key, zero moving parts afterward.

**Is my key safe?**
The field is marked secret, so Apify stores it encrypted and this Actor never logs it. The key's only power is what you share with it: a service account with no roles can touch nothing except sheets explicitly shared with its email. You can revoke the key any time in Google Cloud Console.

**Does it work with sheets on a Google Workspace domain?**
Yes, as long as the domain allows sharing with the service-account email. If your admin restricts external sharing, ask them to allow it or create the service account inside your own Workspace project.

**Can it write to a specific range or cell offset?**
Export always writes tables starting at A1 (header) with data below. For partial-range surgery, use import to read, transform in your own Actor, and re-export.

**What about datasets bigger than 100K rows?**
Fine, both ways. Exports are written in batches of 5,000 with ~1.1s pacing that stays under Google's binding quota (60 write requests/min/user); a 100K-row export is ~20 write calls. Imports and upsert reads are paged in 20,000-row chunks. The practical ceiling is Google's own: 10 million cells per spreadsheet.

**What happens to rows without a key column value in upsert mode?**
They are skipped and counted in the run summary, not appended. Appending them would silently duplicate those rows on every scheduled run.

**What happens on a Google outage or rate limit?**
429 and 5xx responses are retried up to 5 times with exponential backoff and jitter. Only a persistent failure surfaces, and the message says whether to wait, split the export, or check Google's status page.

**Import gives me numbers as strings?**
Set *Cell value type* to `typed` (the default). If you see strings anyway, your sheet cells are literally text-formatted; either fix the sheet formatting or keep `formatted` and parse downstream.

**Can I try it without a Google account?**
Yes. Turn on *Test mode* and run: the whole pipeline executes against an in-memory fake Sheets API, so you can see the run flow, output summary, and dataset handling before doing any setup.

**Is scraped content safe to write into my sheet?**
Yes. Under the default USER\_ENTERED mode, any cell that starts with `=`, `+` or `@` is escaped to literal text, so a scraped page can never plant an `=IMPORTXML` or `=HYPERLINK` formula in your spreadsheet. RAW mode stores strings verbatim and never executes anything.

Whether you got here searching for a Google Sheets export for your dataset, a Google Sheets import into Apify, or just a Sheets integration that does not break on OAuth, this Actor is built to be the one you stop thinking about.

# Actor input Schema

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

<b>export</b> writes an Apify dataset to a Google Sheet. <b>import</b> reads a sheet range into a new Apify dataset.

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

Paste the <b>entire</b> JSON key file of a Google service account (starts with <code>{"type": "service\_account", ...}</code>). Then share your spreadsheet with the service account's <code>client\_email</code> as <b>Editor</b>. Stored encrypted; never logged. Required for every real run — it is only optional so that <b>Test mode</b> works without any Google setup; a real run without it fails immediately with the 3-step setup guide from the README (about 2 minutes, no OAuth, nothing ever expires).

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

The spreadsheet URL (just paste it from your browser) or the spreadsheet ID (the long string between <code>/spreadsheets/d/</code> and <code>/edit</code>).

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

The tab to write to or read from. On export the tab is created automatically if it does not exist. Case-sensitive.

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

The Apify dataset to export. Leave empty to use the run's default dataset — that is what you want when this Actor runs as an integration on another Actor's finished run.

## `exportMode` (type: `string`):

<b>replace</b> clears the tab and writes everything fresh. <b>append</b> adds rows below existing data (new columns are merged into the header). <b>upsert</b> matches rows on the key column: existing rows are updated in place, new ones appended, unchanged ones skipped.

## `keyColumn` (type: `string`):

The field that uniquely identifies a row, e.g. <code>id</code>, <code>url</code>, or a flattened path like <code>user.email</code>. Required when Export mode = upsert.

## `valueInputOption` (type: `string`):

<b>USER\_ENTERED</b>: Sheets parses values as if typed by a user (numbers, dates). For safety, cells starting with <code>=</code>, <code>+</code> or <code>@</code> are escaped to literal text so scraped content can never execute as a formula in your sheet. <b>RAW</b>: values are stored exactly as sent, no parsing.

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

Optional cap on the number of dataset items exported. Leave empty for all.

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

Advanced: a JSON array of objects to export directly, bypassing datasets. Handy for quick tests and for calling this Actor from code.

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

A1 range to read, e.g. <code>A1:F100</code> or <code>A2:D</code>. Leave empty to read the whole tab.

## `headerRow` (type: `boolean`):

When on, the first row of the range provides the field names of the dataset items. When off, fields are named column\_1, column\_2, ...

## `skipEmptyRows` (type: `boolean`):

Rows where every cell is empty are not imported.

## `valueRender` (type: `string`):

<b>typed</b>: numbers and booleans arrive as real types (recommended for automation). <b>formatted</b>: values arrive as the strings you see in the sheet (currency symbols, date formats).

## `resultDatasetIdOrName` (type: `string`):

Optional named dataset to push imported rows into. Leave empty to use the run's default dataset.

## `testMode` (type: `boolean`):

Runs the full pipeline against an in-memory fake Sheets API. No credentials needed, nothing written to Google. For trying the Actor out and for CI.

## Actor input object example

```json
{
  "mode": "export",
  "spreadsheetId": "https://docs.google.com/spreadsheets/d/1qpyC0XzvTcKT6EISywvqESX3A0MwQoFDE8p-Bll4hps/edit",
  "sheetName": "Sheet1",
  "exportMode": "replace",
  "valueInputOption": "USER_ENTERED",
  "headerRow": true,
  "skipEmptyRows": true,
  "valueRender": "typed",
  "testMode": false
}
```

# Actor output Schema

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

JSON summary of the run: mode, rows written/read, rows updated/appended/unchanged/skipped, target spreadsheet and tab.

## `importedRows` (type: `string`):

Import mode only: one dataset item per sheet row, columns as fields, typed values.

# 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("bridgeworks/sheets-bridge").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("bridgeworks/sheets-bridge").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 bridgeworks/sheets-bridge --silent --output-dataset

```

## MCP server setup

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

```

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/nPdhMHt4adXU4TbD5/builds/1MzSpXj7LFMoUKPVb/openapi.json
