# Sheets Exporter Pro (`dock-labs/sheets-exporter-pro`) Actor

Export an Apify dataset as one immutable snapshot into a brand-new, transfer-owned tab of your Google Sheet - resumable and idempotent. No duplicate rows on retries, no silent data loss, never writes into a pre-existing tab.

- **URL**: https://apify.com/dock-labs/sheets-exporter-pro.md
- **Developed by:** [Dock](https://apify.com/dock-labs) (community)
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.20 / 1,000 row exporteds

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

Export an Apify dataset to Google Sheets in a new tab created for that transfer. Google Sheets Exporter Pro verifies previously written ranges before it resumes, preserves spreadsheet-sensitive values, and stops rather than writing into an existing tab.

- **Protect existing tabs.** A new transfer creates its own tab and records Google's numeric sheet ID. It does not select an existing tab as the destination for a new transfer.
- **Resume with verification.** Re-run the same unchanged dataset, spreadsheet, and `transferId` to verify completed ranges before continuing. A completed transfer returns `ALREADY_COMPLETE` without writing its rows again.
- **Keep IDs and scraped text intact.** String IDs such as `"00123"`, large integer IDs, `0`, and `false` are preserved. Formula-like strings are written with Google's `RAW` mode so Sheets stores them as text.

### Export an Apify dataset to Google Sheets

This Apify Google Sheets integration maps each dataset object to one spreadsheet row.

Given this Apify dataset item:

```json
{
  "customer_id": "00123",
  "order_id": 1152921504606846976,
  "active": false,
  "note": "=IMPORTXML(\"https://example.com\", \"//title\")"
}
```

The Actor creates a new tab with this shape:

| customer\_id | order\_id | active | note |
|---|---|---|---|
| 00123 | 1152921504606846976 (text) | FALSE | =IMPORTXML(...) (literal text) |

Nested objects become dotted columns such as `user.name`. Arrays remain JSON in one cell. The Actor also writes one transfer report to the run's default dataset:

```json
{
  "status": "COMPLETE",
  "tabTitle": "export_ffdcbd57a7e5",
  "rowsTotal": 1250,
  "rowsWrittenThisRun": 1250,
  "rowsChargedTotal": 1250
}
```

### Run your Google Sheets export in 3 steps

1. Open the target spreadsheet and share it with `sheets-exporter@dock-labs.iam.gserviceaccount.com` as an **Editor**.
2. Copy the spreadsheet ID: the token between `/d/` and `/edit` in its URL.
3. Choose the source **Apify dataset**, paste the **Google Sheets spreadsheet ID**, and run the Actor.

The first run creates a tab named `export_<derived-id>`. Remove the service-account email from the spreadsheet at any time to revoke future access.

### Prevent duplicate rows and protect existing Google Sheets tabs

Each transfer has a durable manifest and a stable `transferId`. Before writing a batch, the Actor records its intended range. After an interrupted request, it reads that range and compares it with the expected values before continuing.

- A matching range is accepted and not written again.
- An empty reserved range can be written.
- Unexpected content stops the transfer as `NEEDS_RECONCILIATION`; it is not overwritten.
- A completed re-run with the same unchanged source, target, and `transferId` returns `ALREADY_COMPLETE`.

Use one active run per `transferId`. Verification-based recovery reduces duplicate writes; it is not a promise that platform or network failures are impossible.

### Preserve JSON IDs and formula-like text in Google Sheets

For JSON to Google Sheets transfers, the Actor applies these conversion rules:

- String IDs keep leading zeroes: `"00123"` stays `00123`.
- Integers outside JavaScript's safe integer range are stored as text to preserve their digits.
- `0` remains numeric zero and `false` remains Boolean `FALSE`.
- Formula-like strings beginning with `=`, `+`, `-`, or `@` are sent as `RAW` values rather than executed as formulas.
- `null`, a missing value, and an empty string all appear as an empty cell.
- No automatic date, currency, or locale conversion is applied.

### Google Sheets export limits

Version 1 accepts non-empty Apify datasets containing JSON-object rows and enforces these bounds before silently dropping data:

- Up to 100,000 dataset rows.
- Up to 500 columns.
- Up to 49,000 characters in one cell.
- Up to 8 nested object levels.
- Up to Google's 10,000,000 allocated-cell spreadsheet limit.

When `fields` is supplied, only those field paths are exported and every other source field is intentionally excluded. Without `fields`, columns are discovered in stable first-seen order. Use a completed source dataset: a dataset that is still receiving rows is not a consistent snapshot.

Version 1 does not append or synchronize into an existing tab, import Sheets data back into Apify, expand arrays into multiple rows, or reinterpret dates and currencies. A new snapshot needs a new `transferId`; schedules do not generate dated IDs or tab names automatically.

### How much does it cost to export an Apify dataset to Google Sheets?

Pricing is pay per event and includes platform usage:

| Event | Price |
|---|---:|
| Actor start | $0.005 per invocation that reaches billing |
| Row written | $0.0002 per row ($0.20 per 1,000) |

The start fee applies to each invocation, including an `ALREADY_COMPLETE` re-run or a run that fails after billing begins. Data batches written and committed before a later failure may already be charged; rows never written are not submitted as row charges.

Examples:

- 1,000 written rows: `$0.005 + (1,000 × $0.0002) = $0.205`.
- 30 separate 500-row exports: `30 × ($0.005 + 500 × $0.0002) = $3.15`.
- A 20,000-row transfer with `maxChargedRows: 10000` stops before the next batch would exceed the 10,000-row transfer cap. At 10,000 written rows it costs `$2.005`.

`maxChargedRows` caps row charges across that transfer; it does not include Actor start fees. Because writes are batched, a transfer can stop below the requested cap rather than split a batch.

### Google Sheets export inputs

| Input | Required | Meaning |
|---|---:|---|
| `datasetId` | Yes | Source Apify dataset. The Actor requests read access only to this selected dataset. |
| `spreadsheetId` | Yes | Target Google Sheets spreadsheet ID, not the full URL or `gid`. |
| `fields` | No | Exact flattened field paths and column order. Unlisted fields are excluded. |
| `transferId` | No | Stable ID used to resume the same logical transfer. A new ID creates a new tab. |
| `maxChargedRows` | No | Maximum data rows this transfer may write and charge across runs; excludes start fees. |
| `mode` | No | `export`, the only version 1 mode. |

### Google Sheets export statuses and recovery

| Status | Meaning |
|---|---|
| `COMPLETE` | All source rows were committed. |
| `ALREADY_COMPLETE` | The same completed transfer was verified; no data rows were written again. |
| `PARTIAL` | A row-charge cap stopped the transfer before the next batch. Re-run the same unchanged transfer with a higher cap to continue. |
| `NEEDS_RECONCILIATION` | A reserved range or transfer-owned tab differed from the saved manifest. The Actor stopped instead of overwriting it. |
| `FAILED` | Input, access, capacity, source-change, Google API, or platform processing failed. Earlier committed batches can remain in the tab. |

Handled outcomes produce one report record in the run's default dataset. A forced platform termination can end a process before it writes that final record.

### Access and data handling

The Actor runs with limited Apify permissions and requests read access to the selected source dataset. Google access uses a creator-managed service account; sharing a spreadsheet grants that Google identity Editor access until you remove it. The implementation is designed to write only to the spreadsheet ID supplied for the run and only to a tab created for the transfer.

The Actor reads source rows during the run and does not log row contents. To support later recovery, it stores transfer metadata in an Apify key-value store: transfer and spreadsheet IDs, column headers, tab identity, batch checksums and statuses, and charge-journal records. This transfer metadata is retained indefinitely so that any future re-run of the same `transferId` stays safe and idempotent; it contains no row contents. To have a transfer's metadata deleted, open an issue on the Actor with the `transferId` and it will be removed within 3 business days.

Do not submit a dataset or share a spreadsheet unless you are authorized to process its contents. Never place a Google credential in the Actor input.

### Google Sheets exporter FAQ

#### What happens if I run the same export twice?

With the same unchanged dataset, spreadsheet, and `transferId`, a completed transfer returns `ALREADY_COMPLETE`: it writes no data rows again and creates no second tab. The new invocation can still incur its $0.005 start fee. A different `transferId` creates a different snapshot tab.

#### Can the Actor write into an existing tab?

A new transfer creates its own tab. On resume, the Actor targets that tab by Google's numeric sheet ID. If the tab is missing, its header changed, or a reserved range contains unexpected values, the Actor stops instead of overwriting that content.

#### How do I schedule an Apify dataset export?

Trigger the Actor after the source dataset is complete. Reusing the same `transferId` resumes or verifies the same snapshot. To create a new snapshot on each schedule, pass a new deterministic `transferId` for that period, such as `orders-2026-09-06`. The current Actor does not generate dated IDs automatically.

#### Why does the Actor use a service account?

You do not provide a Google OAuth token or service-account key. You grant and revoke the Actor's Google access through the spreadsheet's Share dialog. The creator-managed service account can access every spreadsheet shared with it, so only share an intended target and remove it when no longer needed.

#### What should I do after `PARTIAL`?

Re-run the same unchanged transfer with a higher `maxChargedRows`. Completed batches are verified before the Actor continues. If the source content changed, start a new transfer rather than forcing it into the existing snapshot.

#### Why did the Actor stop at `NEEDS_RECONCILIATION`?

The saved manifest and the Google Sheet did not match. Do not delete a tab until you confirm whether it contains needed data. Use a new `transferId` for a separate snapshot; report a reproducible case without posting sheet contents or credentials in a public issue.

# Actor input Schema

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

ID of the Apify dataset to export. Prefer a completed dataset: a dataset still receiving rows is not a consistent snapshot.

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

The Google Sheets spreadsheet ID (the long token in the sheet URL, NOT the gid tab number).

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

Optional explicit list of flattened field paths (e.g. "user.name") defining the columns, in order. If omitted, fields are discovered from the data in stable first-seen order, capped at 500 columns.

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

v1 supports export only: one immutable snapshot written into a NEW tab created and owned by this transfer (named export\_<derived-id>). The actor never writes into a pre-existing tab. Sync and append modes are explicitly out of v1 scope.

## `transferId` (type: `string`):

Stable identifier for this logical transfer. Reuse it to safely resume or re-trigger the SAME export without duplicate rows or duplicate charges - it targets the same transfer-owned tab. A NEW transfer ID creates a NEW tab. Defaults to a value derived from the dataset and target.

## `maxChargedRows` (type: `integer`):

Optional spending cap. The export stops cleanly (status PARTIAL) before writing a batch that would exceed this many charged rows; unwritten rows are neither written nor charged.

## Actor input object example

```json
{
  "mode": "export"
}
```

# Actor output Schema

## `transferReport` (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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("dock-labs/sheets-exporter-pro").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("dock-labs/sheets-exporter-pro").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 dock-labs/sheets-exporter-pro --silent --output-dataset

```

## MCP server setup

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

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/oxFQ49kpKkpb9goSr/builds/HrJnXhNvCKx9pAX8k/openapi.json
