Google Sheets Export: Append, Upsert & Sync
Pricing
from $0.10 / 1,000 row writtens
Google Sheets Export: Append, Upsert & Sync
Google Sheets export and sync for any Apify dataset, Actor run output or JSON rows. Append, replace or upsert by key column, with a summary of rows appended, updated and unchanged. Share the sheet with our bot email: no Google Cloud setup, no expiring tokens. $0.10 per 1,000 rows.
Pricing
from $0.10 / 1,000 row writtens
Rating
0.0
(0)
Developer
Brasspage
Maintained by CommunityActor stats
0
Bookmarked
3
Total users
2
Monthly active users
14 days ago
Last modified
Categories
Security
Limited permissionsShare
Google Sheets Export: Append, Upsert & Sync
Give it a Google Sheet URL and an Apify dataset ID (or JSON rows). It writes the rows to the tab you name, with a header row made from your data, and returns a summary of what changed. $0.10 per 1,000 rows written. Unchanged rows in upsert mode are free.
| You give it | You get back |
|---|---|
| Sheet URL and tab name | Your rows in that tab, header row added, new fields as new columns |
Dataset ID, for example {{resource.defaultDatasetId}} | Nested fields flattened: price.value becomes column price/value |
Mode: append, replace or upsert by a key column | A summary row: rowsAppended, rowsUpdated, rowsUnchanged |
| Share the sheet with our bot email (2 minutes) | Errors that say what to fix, and retries on Google quota errors |
Example: a scraper dataset row {"url": "https://shop.example/a", "title": "Mug", "price": {"value": 12, "currency": "USD"}} becomes this sheet row:
| url | title | price/value | price/currency |
|---|---|---|---|
| https://shop.example/a | Mug | 12 | USD |
Before you click Start: share your sheet with sheets-sync-test@brasspage-tools.iam.gserviceaccount.com as Editor, put your Apify user ID in a cell named brasspage_owner, and paste the sheet URL. No Google Cloud project, no key. With no input a run only returns this setup note and costs the $0.00005 start. Setup takes about 2 minutes. 11 of 11 runs succeeded in the last 30 days (Apify run stats, Oct 25, 2026).
Use it after a scraper run, on a schedule, or as an integration that runs by itself each time another Actor finishes.
- Share and go: share the sheet with our bot email. No OAuth pop-up, no tokens that expire after 7 days, and it works from the API, schedules, and AI agents.
- Reliable writes: large exports go in chunks, and Google quota errors (429) and server errors retry with exponential backoff.
- Your column order stays the same. New fields become new columns at the end.
What you get
- Your rows in the Google Sheet tab you name, with a header row made from your data. If the tab does not exist, the Actor adds it.
- One summary row per run: the sheet URL, rows read, appended, updated, unchanged and skipped, and the columns written.
- Errors that say what to fix for login and sharing problems. Google quota errors retry by themselves.
- $0.10 per 1,000 rows written. Unchanged rows in upsert mode are free.
A run needs your sheet, so a one-click run with no input only returns a setup note (it costs only the $0.00005 start fee).
How to use
- In Google Sheets click Share, add
sheets-sync-test@brasspage-tools.iam.gserviceaccount.comas Editor, and untick "Notify people". - Type your Apify user ID in one cell and name that cell
brasspage_owner(Data > Named ranges). This proves the sheet is yours. - Open the Actor in the Apify Console, paste the sheet URL and a dataset ID, then click Start.
- To export every run of a scraper, add this Actor on the scraper's Integrations tab with Dataset ID
{{resource.defaultDatasetId}}.
What makes it reliable
Exports to Google Sheets usually fail for the same few reasons. This Actor handles each one:
| Problem | What this Actor does |
|---|---|
| The scraper returned 0 items | The run succeeds and the sheet is not touched. Your integration does not break. |
Google quota error 429 or 500/503, ECONNRESET | Retries up to 8 times with exponential backoff (up to about 4 minutes). |
OAuth token expired (invalid_grant) | The bot login and service-account keys do not expire. |
| The sheet is too small for the new rows or columns | The tab grows first, then the data is written. |
| Large datasets time out | Writes go in chunks of about 2 MB. Each chunk has an exact range, so a retry is safe. |
| Text longer than 50,000 characters (the Google cell limit) | The text is cut to 50,000 characters and the run tells you. |
| The 10,000,000-cell limit | Checked before writing, with a clear message. |
"00123" loses its zeros, "1,5" becomes a date, = runs a formula | Default RAW mode writes values exactly as they are. |
| Append mode rewrites the whole sheet | Append writes only the new rows. Upsert writes only the cells that changed. |
Tab name Sheet1 matches Sheet10 | Exact tab-name match. If the tab does not exist, it is created. |
Setup in 2 minutes
- Open your Google Sheet and click Share.
- Add
sheets-sync-test@brasspage-tools.iam.gserviceaccount.com, select Editor, untick "Notify people", and click Share. - Prove the sheet is yours: type your Apify user ID into one empty cell (find it in Apify Console > Settings > API & Integrations, "User ID"; the first failed run also prints it). Select that cell, click Data > Named ranges, and name it
brasspage_owner. - Paste the sheet URL into Spreadsheet ID or URL, give a data source (a dataset ID or JSON rows), choose a mode, and click Start.
Each run through the bot reads brasspage_owner before it writes. If the range is missing or holds anything other than your user ID, the run stops and writes nothing. Only an editor of your sheet can set that cell, so another Apify account that knows the sheet URL cannot write to it. To stop access at any time, remove the bot email from Share.
Your own Google key (optional)
If you would rather not share the sheet with a shared bot, use a service account of your own:
- Create a Google Cloud project (console.cloud.google.com/projectcreate). You do not need billing.
- Enable the Google Sheets API in that project.
- Under IAM & Admin > Service Accounts, create a service account (no roles), open Keys, and click Add key > Create new key > JSON. Keep the file safe: it is a password.
- Share your sheet with the
client_emailfrom the file as Editor. - Paste the whole file into Your own service-account key (JSON, optional). Apify stores it encrypted.
If your Google Workspace blocks key creation (the policy iam.disableServiceAccountKeyCreation), use the bot login above or a personal Google account.
Run it automatically after another Actor (integration)
- Open the Actor that makes your data (for example, a scraper) and go to its Integrations tab.
- Click Add integration, select Apify Actor, and choose Google Sheets Sync.
- Fill in the spreadsheet and the mode (and your own key, if you use one).
- In Dataset ID, type
{{resource.defaultDatasetId}}. Apify replaces it with the dataset of the run that just finished. - Save. Each time the scraper run succeeds, this Actor writes that run's data to your sheet.
To export the data of one run by hand, open the run, go to its Storage tab, and copy the dataset ID into Dataset ID.
Input examples
Each example was run once on the Apify platform on Oct 27, 2026, against our own test sheet. All three SUCCEEDED. To try one, do the 2-minute setup above, put your sheet URL in spreadsheetId, and paste it into the JSON input tab.
1. Append JSON rows (3.5 seconds, run DcDRAnfjhbm84GvCM). The tab readme-books did not exist, so the Actor made it and wrote the header url, title, author/name, author/born, shelf. Summary: 2 appended. Cost: 2 rows = $0.0002.
{"spreadsheetId": "<your sheet URL>","sheetName": "readme-books","mode": "append","rows": [{"url": "https://www.gutenberg.org/ebooks/1342","title": "Pride and Prejudice","author": { "name": "Jane Austen", "born": 1775 },"shelf": "to-read"},{"url": "https://www.gutenberg.org/ebooks/84","title": "Frankenstein","author": { "name": "Mary Shelley", "born": 1797 },"shelf": "to-read"}]}
2. Upsert by a key column (2.3 seconds, run VD4Ja8Hnod3BBwOBV). Same tab, one book moved to read, one new book. Summary: 1 appended, 1 updated, 1 unchanged. You pay for the 2 rows written; the unchanged row is free.
{"spreadsheetId": "<your sheet URL>","sheetName": "readme-books","mode": "upsert","keyFields": ["url"],"rows": [{"url": "https://www.gutenberg.org/ebooks/1342","title": "Pride and Prejudice","author": { "name": "Jane Austen", "born": 1775 },"shelf": "read"},{"url": "https://www.gutenberg.org/ebooks/84","title": "Frankenstein","author": { "name": "Mary Shelley", "born": 1797 },"shelf": "to-read"},{"url": "https://www.gutenberg.org/ebooks/11","title": "Alice's Adventures in Wonderland","author": { "name": "Lewis Carroll", "born": 1832 },"shelf": "to-read"}]}
3. Export another Actor's dataset, chosen columns only (3.1 seconds, run Vt4za9nSTD1v0337N). The dataset came from a media-transcriber run on a public NASA video. Replace mode wrote the 6 named columns to readme-transcripts. Use the dataset ID from your own run's Storage tab, or {{resource.defaultDatasetId}} in an integration.
{"spreadsheetId": "<your sheet URL>","sheetName": "readme-transcripts","mode": "replace","datasetId": "<dataset ID>","columns": ["mediaUrl","durationSec","language","wordCount","text","srtUrl"]}
Run it from the API:
curl -X POST "https://api.apify.com/v2/acts/brasspage~google-sheets-sync/runs?token=$APIFY_TOKEN" \-H "Content-Type: application/json" \-d '{"spreadsheetId":"1AbCdEf...","mode":"append","rows":[{"name":"A","price":10}]}'
Input fields
| Field | What it does |
|---|---|
spreadsheetId | The spreadsheet URL or ID. |
sheetName | The tab name. Empty = the first tab. A missing tab is created. |
mode | append (default), replace, or upsert. |
keyFields | Field names that identify a row, for example ["url"] or ["store", "sku"]. Upsert needs them. |
serviceAccountJson | Optional: your own service-account key file (secret). Empty = the bot login. |
datasetId | The dataset to export. |
rows | Raw JSON: an array of objects. |
columns | Pick and order the fields to write. Empty = all fields. |
flattenNested | true (default): nested fields become a/b columns. false: nested values are written as JSON text. |
valueInputOption | RAW (default) writes values as they are. USER_ENTERED lets Sheets parse dates, numbers, and formulas. |
oauthClientId, oauthClientSecret, oauthRefreshToken | Optional OAuth login instead of a service account (see the FAQ). |
If you give both, rows wins over datasetId.
Modes
append adds the rows after the last row of the tab. The first run writes the header row. If you also set keyFields, rows whose key is already in the sheet are skipped (dedupe).
replace writes the header and all rows from the top of the tab, then clears the old rows and columns below and to the right. The tab is never empty during the run. If the source is empty, the tab is not cleared.
upsert finds rows by keyFields. If a key is already in the sheet, only the cells that changed are written. If a key is new, the row is added at the end. Columns you added by hand (for example "Notes" or "Status") and formulas in them stay as they are. If the same key appears more than once in the input, the items are merged and the last value wins.
In all modes, new fields become new columns at the end of the header. Existing columns keep their order.
Output example
The Actor writes one summary item to its dataset (and to the OUTPUT record):
{"spreadsheetUrl": "https://docs.google.com/spreadsheets/d/1AbC.../edit#gid=0","spreadsheetTitle": "Price tracker","sheetName": "Products","mode": "upsert","source": "dataset aBcD1234eFgH5678i","rowsRead": 1200,"rowsAppended": 35,"rowsUpdated": 112,"rowsUnchanged": 1053,"rowsSkippedDuplicate": 0,"rowsSkippedNoKey": 0,"rowsWritten": 147,"columns": ["url", "title", "price/value", "price/currency", "inStock"],"durationMs": 4210,"errors": []}
errors lists warnings, for example long text that was cut. A real error fails the run, and the run's status message says what to fix.
Pricing
Pay per event. You pay only for what is written:
| Event | Price |
|---|---|
| Actor start | $0.00005 per run |
| Row written | $0.0001 per row |
Examples: 100 new rows = $0.01. A daily upsert of 2,000 products where 150 changed = $0.015. Rows that did not change, duplicates that were skipped, and the header row are free. A run with the empty default input writes no rows and costs only the $0.00005 start. Set a maximum cost per run in the run options to cap the cost: the Actor writes only as many rows as that limit allows and tells you.
Limits
- Google Sheets allows 10,000,000 cells per spreadsheet, 18,278 columns per tab, and 50,000 characters per cell.
- Google allows about 60 write requests per minute per service account. The Actor writes up to about 2 MB per request, so this is rarely reached. When it is, the Actor waits and retries.
- The Actor reads the dataset and the current tab into memory. The default is 512 MB. If a run on a very large sheet runs out of memory, set the run memory to 1 to 2 GB.
- In
appendandupsertmodes, row 1 of the tab is the header.
FAQ
Why a bot email instead of "Sign in with Google"? The bot is a Google service account: a robot Google user that can open only the sheets you share with it. Its key does not expire, so schedules and integrations keep working. OAuth tokens for apps in "Testing" status expire after 7 days, which is the most common reason Google Sheets exports stop working.
Is the bot login safe?
The bot can open only the sheets you share with it, and you can remove it from Share at any time. Its key is kept in Apify secret storage and never shown in logs. Before each write, the bot checks that the sheet's brasspage_owner cell holds the Apify user ID of the account that runs it, and refuses any other account. If you use your own key instead, Apify stores it encrypted and does not show it in logs.
Can I use OAuth instead?
Yes. Create an OAuth client (type "Desktop app") in your own Google Cloud project, get a refresh token with the scope https://www.googleapis.com/auth/spreadsheets (for example with the OAuth 2.0 Playground and "Use your own OAuth credentials"), and fill in oauthClientId, oauthClientSecret, and oauthRefreshToken. Publish the OAuth app to "In production", or the token expires after 7 days.
Can I write to an Excel file in Google Drive? No. Open the file and use File > Save as Google Sheets, then use the new spreadsheet.
How do I write nested data?
Leave flattenNested on. Use columns to pick only the fields you need, for example ["title", "price/value", "images/0"].
Can an AI agent use it?
Yes. To give an AI agent this one tool, connect it to the Apify MCP server at https://mcp.apify.com?tools=brasspage/google-sheets-sync. The agent passes rows and the sheet, and gets the sync summary back.
Does it read data from Google Sheets? No. This Actor only writes to Google Sheets.
Troubleshooting
| Message starts with | Fix |
|---|---|
AUTH: No access to the spreadsheet | Share the sheet with the email in the message (the bot, or your key's client_email) as Editor. |
AUTH: The shared Google login writes only to sheets | Add the brasspage_owner named range with your Apify user ID (see Setup). |
AUTH: The named range brasspage_owner | Make that cell hold only your Apify user ID, nothing else. |
AUTH: The Google Sheets API is turned off | Open the link in the message and click Enable. Wait 1 to 2 minutes. |
AUTH: "serviceAccountJson" is not valid JSON | Paste the whole key file, from the first { to the last }. |
AUTH: This JSON is an OAuth client file | You pasted the wrong file. Create a key under Service Accounts > Keys. |
AUTH: Google rejected the service-account key | The key was deleted. Create a new key and paste it. |
AUTH: The OAuth refresh token is expired or revoked | Make a new refresh token, and publish your OAuth app to "In production". |
Spreadsheet "..." was not found | Check the ID in the URL, and share the sheet with the bot email (or your service account). |
This file is an Excel file | Use File > Save as Google Sheets. |
Key field "x" is not in the data | Check the spelling in keyFields. Nested fields use a slash: product/id. |
This run cannot read dataset | Put the dataset ID into Dataset ID. In an integration, use {{resource.defaultDatasetId}}. |
No data source | Set datasetId or rows. In an integration, set datasetId to {{resource.defaultDatasetId}}. |
Issues and support
Found a bug or need a feature? Open an issue in the Actor's Issues tab, with the run URL if you have one. We check it every day and reply there.
Related Actors
Other tools from the same developer, with the same pay-per-result pricing:
- ATS Jobs API: every open job from Greenhouse, Lever, Ashby, SmartRecruiters, Workable, Recruitee and Personio boards, with new-job alerts.
- Audio & Podcast Transcription: podcast episodes and audio or video files to text, SRT and VTT for $0.004 per minute.
- Website Contacts & Tech Stack: public emails, phones, social links, CMS and pixels for a list of websites.
- PDF to Markdown OCR: scanned PDFs and images to clean Markdown with tables, for $0.006 per page.