How to sync data from Coda to Google Sheets (and vice versa) with Google Apps Script tutorial
Tags: googlesheets, coda, googleappsscript, tutorial
Published: 2020-05-04 20:31:40

---
title: How to sync data from Coda to Google Sheets (and vice versa) with Google Apps Script tutorial
description: Keep your data synced across your Coda docs and Google Sheets so you don't have to copy and paste anymore.
published: true
date: 2020-05-04 17:56:22 UTC
tags: google sheets, coda, google apps script, tutorial
canonical_url: https://coda.io/@atc/how-to-sync-data-from-coda-to-google-sheets-and-vice-versa-with-google-apps-script-tutorial
cover_image: https://p-ZmF7dQ.b0.n0.cdn.getcloudapp.com/items/eDu6gJdK/sync_cover.jpg?v=ac30e3861b0816b32558bc9609efd701
---

## Two new scripts

Last year I published [a tutorial](https://coda.io/@atc/how-to-sync-data-between-coda-docs-and-google-sheets-using-googl) on how to sync data between two Coda docs and data between two Google Sheets. What was missing from the tutorial was how to sync data between a **Coda doc** and a **Google Sheet**. Writing these scripts was definitely more challenging than the original script I wrote for syncing two Coda docs since the data model for Coda is different from Google Sheets. Please read the [caveats](https://coda.io/d/How-to-sync-data-from-Coda-to-Google-Sheets-and-vice-versa-with-_dv4i9X8bdFe/How-to-sync-data-from-Coda-to-Google-Sheets-and-vice-versa-with-_su8ir#_lueHe) below about these scripts to learn about some of the roadblocks I encountered when writing these scripts.

If you are reading this, chances are you have a lot of experience with Google Sheets, Coda, and perhaps the [Coda API](https://coda.io/developers/apis/v1beta1). I’m going to skip the introduction to Coda as I did with the [last tutorial](https://coda.io/@atc/how-to-sync-data-between-coda-docs-and-google-sheets-using-googl) and get straight to the point on how you can:

1. Sync data from [Coda -> Google Sheets](https://coda.io/d/How-to-sync-data-from-Coda-to-Google-Sheets-and-vice-versa-with-_dv4i9X8bdFe/Coda-Google-Sheets_suhDq)
2. Sync data from [Google Sheets -> Coda](https://coda.io/d/How-to-sync-data-from-Coda-to-Google-Sheets-and-vice-versa-with-_dv4i9X8bdFe/Google-Sheets-Coda_suKcq)

_If you want to skip right to using the Google Apps Scripts, go to the other two pages in this doc (mentioned above) or go to_ [_this repo_](https://github.com/albertc44/coda-google-apps-script) _which contains all four scripts for syncing data (PRs welcome). Here are two video tutorials if you prefer a visual tutorial._

### Coda to Google Sheets

{% youtube mAdAe8GVCdA %}

### Google Sheets to Coda

{% youtube xVWu9jdBm_U %}

## Features

There are some limitations to the scripts which I’ll discuss later on in this blog post, but these are the main features for each script:

### [Coda -> Google Sheets](https://coda.io/d/How-to-sync-data-from-Coda-to-Google-Sheets-and-vice-versa-with-_dv4i9X8bdFe/Coda-Google-Sheets_suhDq#_luV4e)

- New rows that get **added or deleted** in your Coda table will also get added or deleted in Google Sheets
- Existing rows that get **updated** in Coda will also get updated in Google Sheets
- You can **re-arrange the columns** in your Google Sheet and the sync will still sync the appropriate columns in your Google Sheet
- You can **add or insert new columns** in your **Google Sheet** and write formulas in these new columns
- You can **add or insert new columns** in your table in **Coda** and these columns won’t get synced to Google Sheets (unless you create a new column in Google Sheets with the same column name as the one in your Coda table)

### [Google Sheets -> Coda](https://coda.io/d/How-to-sync-data-from-Coda-to-Google-Sheets-and-vice-versa-with-_dv4i9X8bdFe/Google-Sheets-Coda_suKcq)

- New rows that get **added or deleted** in your Google Sheet worksheet will also get added or deleted in your Coda table
- Existing rows that get **updated** in your Google Sheet worksheet will also get updated in Coda
- You can **sort and filter** the rows in your target Coda table and the script will still add, delete, and update the appropriate rows in Coda
- You can **add rows** to your Coda table and not get them deleted on the sync by adding a “Do not delete” [checkbox column](https://help.coda.io/en/articles/1235680-overview-of-column-formats#types-of-column-formats) in your Coda table that is set to `true` (more about this later in the post)

Some of the features in the [Coda -> Google Sheets](https://coda.io/d/How-to-sync-data-from-Coda-to-Google-Sheets-and-vice-versa-with-_dv4i9X8bdFe/Coda-Google-Sheets_suhDq) script also apply to the [Google Sheets -> Coda](https://coda.io/d/How-to-sync-data-from-Coda-to-Google-Sheets-and-vice-versa-with-_dv4i9X8bdFe/Google-Sheets-Coda_suKcq) script, but I haven’t fully tested every use case. If you see any bugs, please add them to the repo’s [issues list](https://github.com/albertc44/coda-google-apps-script/issues).

## Setup: Coda to Google Sheets script

Starting in [line 9](https://github.com/albertc44/coda-google-apps-script/blob/master/coda_to_sheets.js#L9) to [line 14](https://github.com/albertc44/coda-google-apps-script/blob/master/coda_to_sheets.js#L14) of the [coda_to_sheet.js](https://github.com/albertc44/coda-google-apps-script/blob/master/coda_to_sheets.js#L10) script, you’ll need to enter in some of your own data to make the script work. Step-by-step:

1. Go to [script.google.com](https://script.google.com/home) and create a new project and give your project a name.
2. Go to Libraries then Resources and paste the following string of text/numbers into the library field: `15IQuWOk8MqT50FDWomh57UqWGH23gjsWVWYFms3ton6L-UHmefYHS9Vl`.
3. Click Add and then select a version of the library to use (as of May 2020, version 8 is the latest)
4. Copy and paste the [entire script](https://github.com/albertc44/coda-google-apps-script/blob/master/coda_to_sheets.js) into your Google Apps Script project and click File then Save.
5. Go to your Coda [account settings](https://coda.io/account), scroll down until you see “API SETTINGS” and click Generate API Token. Copy and paste that API token into the value for `YOUR_API_KEY` in the script. _Note: do not delete the single apostrophes around_ `YOUR_API_KEY`.
6. Get the the doc ID from your Coda doc by copying and pasting all the characters after the `_d` in the URL of your Coda doc (should be about 10 characters). You can also use the _Doc ID Extractor_ tool in the [Coda API docs](https://coda.io/developers/apis/v1beta1#section/Using-the-API/Resource-IDs-and-Links). Copy and paste your doc ID into `YOUR_SOURCE_DOC_ID`.
7. Go back to your [account settings](https://coda.io/account) and scroll down to the very bottom until you see “Labs.” Toggle “Enable Developer Mode” to ON.
8. Hover over the table name in your Coda doc and click on the 3 dots that show up next to your table name. Click on “Copy table ID” and paste this value into `YOUR_SOURCE_TABLE_ID`.
9. To get your Google Sheets ID, get all the characters after `/d/` in your Google Sheets file up until the slash and paste this into `YOUR_GOOGLE_SHEETS_ID`. See [this link](https://stackoverflow.com/a/36062068/1110697) for more info.
10. Write in the name of the worksheet from your Google Sheets file where data will be synced into in the `YOUR_GOOGLE_SHEETS_WORKSHEET_NAME` value.
11. In Google Sheets, create a new column name at the end of your column headers called something like `Coda Source Row URL` and make sure there is no data in that column below the header. Write that column name in `YOUR_SOURCE_ROW_URL_COLUMN_NAME`.
12. Go back to Google Apps Script, click on the Select function dropdown in the toolbar, and select `runSync`. Then click the play ▶️ button to the left of the bug 🐞 button. This should copy over all the data from your Coda doc to Google Sheets.
13. To get the script to run every minute, hour, or day, click on the clock 🕒 button to the left of the ▶️ button to create a [time-driven trigger](https://developers.google.com/apps-script/guides/triggers/installable#time-driven_triggers).
14. Click Add Trigger, make sure runSync is set as the function to run, “Select event source” should be `Time-driven`, and play around with the type of time based trigger that fits your needs. I like to set the “Failure notification settings” to `Notify me immediately` so I know when my script fails to run.

## Setup: Google Sheets to Coda script

Most of the steps above apply to the [sheets_to_coda.js](https://github.com/albertc44/coda-google-apps-script/blob/master/sheets_to_coda.js) script as well but there are few extra features.

1. You can follow steps 1–10 above to fill out [line 12](https://github.com/albertc44/coda-google-apps-script/blob/master/sheets_to_coda.js#L12) to [line 18](https://github.com/albertc44/coda-google-apps-script/blob/master/sheets_to_coda.js#L18) in the script (except [line 14](https://github.com/albertc44/coda-google-apps-script/blob/master/sheets_to_coda.js#L14) mentioned in the next step). The main difference is that “SOURCE” and “TARGET” are flipped around since you are now syncing from a _source_ Google Sheet to a _target_ Coda doc.
2. Your Coda table _cannot_ have a column named `Coda Row ID`. If you need to use a column with this name, replace the `TARGET_ROW_ID_COLUMN` variable with another value.
3. If you have _edit access_ to the Google Sheet, follow step 11 above and write in the column name in `YOUR_SOURCE_ROW_URL_COLUMN_NAME`.
4. If you want the ability to add rows to your Coda table and NOT have these rows deleted every time the sync runs, create a column in your Coda table and name it `Do not delete`. This column should be a checkbox column format and you will check the box for every row you manually add to your Coda table that you want to keep in that table. Otherwise, the script will delete that row and always keep the Coda table a direct copy of what’s in your Google Sheets file. If you change the name of this `Do not delete` column, you must edit the value of the `DO_NOT_DELETE_COLUMN` variable in [line 22](https://github.com/albertc44/coda-google-apps-script/blob/master/sheets_to_coda.js#L22) of the script as well.
5. If you want the script to completely delete and re-write the rows in your Coda table each time the script runs, set the `REWRITE_CODA_TABLE` to `true` in [line 23](https://github.com/albertc44/coda-google-apps-script/blob/master/sheets_to_coda.js#L23). This may make the script run faster, but may not be faster for larger tables (few thousand rows). For Google Sheets files where you only have _view-only access_, this setting will automatically get set to `true`.
6. Follow steps 12–14 [above](https://coda.io/d/How-to-sync-data-from-Coda-to-Google-Sheets-and-vice-versa-with-_dv4i9X8bdFe/How-to-sync-data-from-Coda-to-Google-Sheets-and-vice-versa-with-_su8ir) to set up your time-driven trigger.

## Use cases with Google Sheets

Some of the most common use cases for integrating your application with Google Sheets can be found in the [G Suite Marketplace](https://gsuite.google.com/marketplace/category/works-with-spreadsheet) for Google Sheets. From a business perspective, being able to visualize your data in Google Sheets allows you to slice and dice your data in ways you cannot do in on platform like Salesforce, for instance (FYI there’s a Salesforce [add-on](https://support.google.com/docs/answer/9073952?co=GENIE.Platform%3DDesktop&hl=en) for Google Sheets).

![](https://cdn-images-1.medium.com/max/530/0*zTAxg1i5hOBqEDrD)

The opposite is true too. Your team or company’s data may be stored in a Google Sheet but the data just sits there without being “actionable.” Let’s say you have a bunch of customer information and you want to create mailing labels with your customers’ names and addresses. Being able to “export” your data from Google Sheets into a mail merge application like Avery will make it easy to create the mailing labels you need.

Then there’s the pinnacle of productivity in Google Sheets: _keeping data synced between your application and Google Sheets at all times_.

When Google Sheets first came out, it was a game-changer since changes you make on your browser are instantly reflected in your colleague’s file. We have come to expect this with tools we use in the browser. But having data synced between Google Sheets and your other applications at all times is less common, and this is why the [Google Sheets API](https://developers.google.com/sheets/api) is so important. From a Coda perspective, there are several use cases you might want to keep your Coda doc synced with a Google Sheet (and vice versa):

### Data synced from your Google Sheet

- **HR & recruiting**  — All your candidates are stored in a Google Sheet but you want to be able to move candidates through different stages in the interviewing pipeline and Google Sheets isn’t sufficient for your needs. Having all your candidates in a table in Coda means you can use templates like [this one](https://coda.io/@evanatcoda/coordinating-candidates) to manage candidates more effectively.
- **E-commerce and ERP**  — Orders, customers, and POs may all be different tabs in a Google Sheet that gets updated through Shopify or some other e-commerce platform. In order to _manage_ your e-commerce business, you may want to see charts, calendar of shipments, and reports that Google Sheets cannot provide easily. Syncing the data from Google Sheets to Coda means you can do ERP properly (see [this template](https://coda.io/@wilson-silva/mini-e-commerce-erp) as an example).
- **Customer Feedback**  — You may have a ticketing system like Zendesk or Intercom and all feedback lands in a Google Sheet somewhere. You can do some basic analytics in the Google Sheet but to _reply_ to the feedback means you have to go into Gmail and start replying to customers. If your customer feedback is all in a Coda doc, you can run analytics _and_ send emails using the [Gmail Pack](https://coda.io/packs/gmail) (see [this template](https://coda.io/@hales/customer-feedback-hub)).

### Data synced to your Google Sheet

- **3rd-party vendor reporting**  — Your vendors may not be using Coda yet, but you have all your vendor data in Coda and need to send them the data in a format they prefer. While you could [publish your Coda doc](https://help.coda.io/en/articles/3727616-intro-to-publishing), the vendor still wants the data in a Google Sheet you have edit access to.
- **Data “backup”**  — Your team may create thousands of rows of data every quarter in a Coda doc and want to start each quarter “fresh.” Coda docs grow with your teams and they may get slow as you add in more functionality, so having a backup of your data in Google Sheets is another reason to sync data from your Coda doc to Google Sheets.
- **Finance & Accounting**  — Most internal finance and accounting functions still use Excel and spreadsheets for month-end reporting, taxes, and other business-critical activities. As your data grows in Coda, you can keep your finance counterparts in the loop by having your data synced to a Google Sheet which your finance team can use for their reporting and forecasting purposes.

## Setting up Google Apps Scripts

Before you start using the scripts to sync data from Coda to Google Sheets or vice versa, you need to have Google Apps Script setup correctly. Just navigate to [script.google.com](http://script.google.com) and click on **New Project**. You’ll land in the GAS script editor. At this point, click on **Resources→Libraries** in the toolbar and you’ll want to paste in the following Coda library for Google Apps Script:

```
15IQuWOk8MqT50FDWomh57UqWGH23gjsWVWYFms3ton6L-UHmefYHS9Vl
```

After you add the library, you can pick a version of the library to use (I just picked the latest version to take advantage of all the latest features in Coda’s API):

![](https://cdn-images-1.medium.com/max/802/0*aquaa4jlWyyqtmP0)<figcaption><em>Add Coda’s library for Google Apps Script</em></figcaption>

## Syncing a Coda doc to Google Sheets

Setting up the script for syncing a table from a Coda doc to a Google Sheets requires a few simple inputs. I walk through how to get some of these inputs in my [previous tutorial](https://coda.io/@atc/how-to-sync-data-between-coda-docs-and-google-sheets-using-googl), so read that if you have any questions on how to get the following inputs:

- **Coda doc ID:** This is the string of characters after the `_d` in the URL of your Coda doc
- **Coda table ID:** The unique ID for the table you want to sync from in Coda. If you have _Enable Developer Mode_ turned on in your account settings, you can get the table ID by simply clicking the 3 dots next to your table:

![](https://cdn-images-1.medium.com/max/474/0*97biLRrR-KilFujj)

- **Google Sheet ID**  — This is the string of characters after the `/d` in the URL of your Google Sheet (see [documentation here](https://developers.google.com/sheets/api/guides/concepts#spreadsheet_id) on how to get this ID).
- **Google Sheet worksheet name**  — Name of the individual worksheet in your Google Sheet you want to sync data _into_ from your Coda doc
- **Source Row Column**  — This is the only customization you’ll have to do to your Google Sheet. You’ll need to add a column (typically the last column in your Google Sheet) that’s called something like `Coda Source Row URL`. This is the name used in the [script](https://coda.io/d/How-to-sync-data-from-Coda-to-Google-Sheets-and-vice-versa-with-_dv4i9X8bdFe/Coda-Google-Sheets_suhDq). This is an **important** column to have in your Google Sheet since it will store the unique URL to a row in your Coda table. More about this later.

Once you have these inputs, you’re ready to get started with syncing your data!

### Column names in Google Sheets

Try to keep the name of the columns in your Google Sheet the same as the columns in your Coda table. All the columns you want to sync from your Coda table to the Google Sheet should have its own column.

The one exception is the `TARGET_SHEET_SOURCE_ROW_COLUMN` variable which you’ll see in the script. Whatever value you put in this variable should also be the name of the column in your Google Sheet. You should put this column at the end of your table in Google Sheets like so:

![](https://cdn-images-1.medium.com/max/626/0*d1_4Kqf16DB26Kls)<figcaption><em>Source row column to put in your Google Sheet</em></figcaption>

This column will be overwritten by the Google Script with the unique source row URL from Coda (every row in a Coda table has a unique identifier). The reason why we need this column for the source row URL is so that the script knows which rows have been added to the Google Sheet so that if you delete any rows in the _source_ Coda doc, those rows can be deleted in the _target_ Google Sheet. This brings me to a quick aside about the benefits of these source row URLs (these are called `browserLink`s in the [API](https://coda.io/developers/apis/v1beta1)).

### A unique row identifier

If you are a heavy user of Google Sheets, you may find yourself creating a “unique ID” column in table so that when you reference that row somewhere else in your Google Sheet, you can do a `VLOOKUP` to pull all the data related to that row. Sometimes you can get away with a column of data (maybe it’s a customer name, task name, or project name). For instance, in this screenshot the unique ID is the `StaffID` column:

![](https://cdn-images-1.medium.com/max/753/0*mk6vx6I3lXyFAWbo)<figcaption><em>Unique ID column in Google Sheets</em></figcaption>

To cover the cases where your table does not have a unique ID, the script puts the unique row URL from Coda into the `TARGET_SHEET_SOURCE_ROW_COLUMN` to act as the unique identifier. The [Google Sheets -> Coda](https://coda.io/d/How-to-sync-data-from-Coda-to-Google-Sheets-and-vice-versa-with-_dv4i9X8bdFe/Google-Sheets-Coda_suKcq) script also utilizes this column (assuming you have edit access to the Google Sheet). In lieu of this unique ID column, there’s no way for the script to know which rows have been added to the Google Sheet from Coda since there’s _no native row ID system in Google Sheets_ (see [this thread](https://stackoverflow.com/questions/38114591/is-it-possible-to-access-row-id-of-a-google-spreadsheet)).

### Fabricating a unique ID in Google Sheets

One alternative to fabricate this unique identifier in your data set is to concatenate a bunch of columns together in hopes that this new column will be the unique ID for that row:

![](https://cdn-images-1.medium.com/max/1024/0*pmEnLZpCjlYxMwsn)<figcaption><em>Creating your own unique ID in Google Sheets</em></figcaption>

In the above screenshot, `Feature` is actually a pretty unique column of data. But to be 100% sure, there’s a `Fabricated ID` column which concatenates `Feature`, `Team`, and `Milestone` to create a “more unique” ID in the event there are two `Features` with the same name. This is not a perfect method due to two reasons:

1. The fabricated ID column might not be unique enough and it might be duplicated in other rows (which means you would have to concatenate more columns of data to fabricate that unique ID)
2. The columns you have concatenated may change (in this case, the `Team` or `Milestone` may change which would ruin the uniqueness of the ID)

In a previous life as a financial analyst, I employed this fabricated ID trick quite often but I had to choose the columns wisely. Typically in a report that has a time series, this would involve picking a dimension (e.g. west region), metric (e.g. sales), and the date for that specific row. This worked for static reports where data wasn’t getting deleted or updated too often. It’s a lot more risky to utilize this strategy with a shared Google Sheet with your team where data is constantly changing. Choose your columns wisely if you go down this path.

![](https://cdn-images-1.medium.com/max/471/0*n7qBiAsD7_s-VuIG)

### Mixing columns in Coda

The advantages of having a unique identifier for the rows in Coda also applies to columns in Coda as well (this benefit is realized in the [Google Sheets -> Coda](https://coda.io/d/How-to-sync-data-from-Coda-to-Google-Sheets-and-vice-versa-with-_dv4i9X8bdFe/Google-Sheets-Coda_suKcq) script). For syncing Coda to Google Sheets, the script _has_ to use the actual names of the columns in Google Sheets since there’s also no _native column ID in Google Sheets_. This means if your column in Coda is named `Projects` but you accidentally misspell the column name in Google Sheets to `Project`, the data will not sync over correctly from Coda to Google Sheets.

One feature of the script is that you can re-order the columns in Coda and the data will still sync over correctly based on the column names. So your tables in Coda and Google Sheets could be organized like this, and the sync would still work:

![](https://cdn-images-1.medium.com/max/1024/0*2Fa9ftC-aFZbGPEd)

The `sortCodaTableCols()` [function](https://github.com/albertc44/coda-google-apps-script/blob/master/coda_to_sheets.js#L167) re-arranges the columns in Coda to reflect the order of the columns in Google Sheets by simply looking for the column name in Coda:

```
var headerCodaTable = sourceRows[0]['cells'].map(function(row) { return row['column'] });
  var sheetsColOrder = [];

  headerRow.map(function(col) {
    sheetsColOrder.push(headerCodaTable.indexOf(col))
  })
 
  var sortedSourceRows = sourceRows.map(function(row) {
    var cells = sheetsColOrder.map(function(col) {
      if (col == -1) {
        return {        
          column: null,
          value: null,
        }
      } 
      else {
        return {
          column: headerCodaTable[col], 
          value: row['cells'][col]['value'],
        }       
      }
    });
    return {cells: cells}
  })
  return sortedSourceRows;
```

This means you can have your own “custom” columns in Coda or Google Sheets which can even contain formulas, and they won’t corrupt the sync from `Task`, `Team`, and `Project` to their respective columns in Google Sheets. As long as these custom column names in Coda or Google Sheets don’t show up in the other platform, then you can do whatever you want with these custom columns:

![](https://cdn-images-1.medium.com/max/1024/0*V88Z7F8N8RFJ_2jq)

This could be useful if you work with a vendor who needs to see data in a Google Sheet to perform certain calculations that could be meaningful to them but don’t really matter to you and your Coda doc. As long as there isn’t a column name in the Google Sheet that matches the name of a column in your Coda table, then everything will work as intended.

### Adding and deleting rows

The main `runSync()` function runs two other functions: `addDeleteToSheets()` and `updateSheet()`. The logic here is to _add_ any new rows from Coda to Google Sheets and _delete_ any rows from Google Sheets that were deleted from Coda. As mentioned [above](https://coda.io/d/How-to-sync-data-from-Coda-to-Google-Sheets-and-vice-versa-with-_dv4i9X8bdFe/How-to-sync-data-from-Coda-to-Google-Sheets-and-vice-versa-with-_su8ir), the script uses a `TARGET_SHEET_SOURCE_ROW_COLUMN` to keep track of all the unique rows that need to be synced from Coda to Google Sheets.

An added benefit of using this “source row column” in Google Sheets is that you can add new rows of data to Google Sheets manually and leave the “source row column” blank. When the sync runs, the script essentially skips these new rows because they don’t have a URL that maps to an existing row in Coda. I’m not sure about the exact use case for when you would want to do this, but perhaps your Coda doc keeps track of sales from a store and your accounting team gets the data synced to a Google Sheet like this:

![](https://cdn-images-1.medium.com/max/1024/0*UJYZlR8xrQDCFBNL)

The columns in yellow are the ones that get synced from your Coda doc. The first 3 rows get synced correctly because you see values in the `Source Row URL` column. The accounting team realizes that there are more sales that were not accounted for and don’t exist in your Coda doc. They might manually add rows 5 and 6 and have a column they use internally called `Manual Enter` to keep track of the rows they are manually adding to the Google Sheet. When the sync runs next, rows 5 and 6 won’t get overwritten or deleted because they left the `Source Row URL` column blank.

### Updating rows

The `addDeleteToSheets()` function was relatively simple to write, but `updateSheet()` was much more difficult given that rows in Google Sheets might be sorted in all kinds of ways. Additionally, I felt that scanning the entire Google Sheet for a source row URL and then scanning each column value to see if an update is needed was inefficient. Even if you have only 100 rows in your Coda doc that you want to sync to Google Sheets, that means there could potentially be 10,000 comparisons just for the row URLs alone every time the sync runs.

One option I considered was just blowing up the entire list of data in Google Sheets first (deleting all the rows) and re-writing the data from Coda to Google Sheets. This also didn’t feel right because for larger tables this could potentially hit Google Apps Script [rate limits](https://developers.google.com/apps-script/guides/services/quotas) and would prevent the need for the `addDeleteToSheets()` function, prevent the need for the “source row column” in Google Sheets, and wouldn’t allow the user to manually add rows to the Google Sheet because those rows would get wiped out on the sync.

![](https://cdn-images-1.medium.com/max/800/0*6GR_77Ms6rdGdvRZ)

My thinking was to create two 2-D tables that were sorted exactly the same. The first table contains the rows from Coda that also exist in Google Sheets. The second table contains the rows in Google Sheets. The tables would contain the same number of rows and columns so you could then do a sequential comparison between the source Coda table and the target Google Sheet and see if there are any updates that need to be made in the Google Sheet.

The first thing to do was to convert the row objects in Coda to a 2-D table that is more similar to Google Sheets’ row objects. The `convertValues()` [function](https://github.com/albertc44/coda-google-apps-script/blob/master/coda_to_sheets.js#L239) “flattens” the Coda row object so that each row object simply contains an array of column values:

![](https://cdn-images-1.medium.com/max/1024/1*xhs_-oJfl0SUjoqWJ8eKWg.png)

Most of the work in these scripts is actually just data munging so that the data is in format that is acceptable for Coda and Google Sheets. Once the tables are sorted in the same order in terms of rows and columns, the script can now check cell by cell if there are any chances that need to be synced over to Google Sheets.

I felt this sequential comparison of cells between the Coda and Google Sheets table was more performant than scanning for each row URL. The number of comparisons between the source and target tables is limited to the number of “cells” in either table. In this example, the script would only have to make 15 comparisons before figuring out that there are three cells in Coda that have been updated and need to be synced over to Google Sheets:

![](https://cdn-images-1.medium.com/max/580/0*nkAY0g7Eau1rdGvB)

While this may seem like a performance boost, there is a lot of pre-processing to get the rows sorted correctly, so the net result might be same in terms of rows and cells scanned. There much more smarter people out there who understand sorting algorithms, so there may be an even more efficient approach here 🤷‍♂️.

### A little helper sort function

In order to get the tables sorted perfectly before doing the cell by cell comparison, I needed to figure out a way to sort an array of arrays by some value. In this case, we have a bunch of arrays of column values that represent our rows, and the unique ID we want to sort on is the source row URL:

![](https://cdn-images-1.medium.com/max/670/0*CeoxN0dXKQ6d2a3E)<figcaption><em>How do we sort each row object by the 7th element (row URL)?</em></figcaption>

I created this little `sortArray()` [function](https://github.com/albertc44/coda-google-apps-script/blob/master/coda_to_sheets.js#L239) that’s one of the workhorses in the script. It seems like such a common problem and I was surprised there wasn’t a built in sort function to sort an array of arrays (or maybe I just didn’t search hard enough). So if I want to sort the `targetRows` object below which contains all the rows in my Google Sheet, I run the `sort()` function on it and pass in the `sortArray()` function and the returned `sortedTargetRows` object is…as you expected…sorted by the source row URL:

```
var sortedTargetRows = targetRows.**sort(sortArray)**;

function sortArray(a, b) {
  var x = a[rowURLIndex];
  var y = b[rowURLIndex];
  if (x === y) {
    return 0;
  }
  else {
    return (x < y) ? -1 : 1;
  }
}
```

One thing I learned about the `sort()` [function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) is that if you pass in what they call a `compareFunction` (in my case the `sortArray()` function), to sort values by alphabetical order, it actually sorts in alphabetical order for values with _uppercase_ letters followed by _lowercase_ letters. Here is a list of values and how you expect them to be sorted versus how the `sort()` function actually sorts stuff:

![](https://cdn-images-1.medium.com/max/368/1*N_mFvNDMSGAZxAzgbxxmwA.png)<figcaption><em>WTF?</em></figcaption>

Now if you sort this list of values in a spreadsheet or Coda table, you’ll get the results in the `What you expect` column. I couldn’t figure out why the sorted values didn’t match up with what I expected after sorting the values in Google Sheets. Then after some debugging I realized this is the default behavior of the `sort()` function in [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort). A common workaround is to apply the `toUpperCase()` function to the value so that you are doing a case-insensitive sort. Unfortunately, this won’t work for the script because it’s possible for a table in Coda to have two row IDs with the same order of six characters but just be capitalized differently (e.g. a row ID of `NPmgrG` and `NPMGRG` could exist in the same table).

![](https://cdn-images-1.medium.com/max/526/0*xnsS55bL1khGm9Do)

In our case, we need to find a _case-sensitive_ sort to account for the uniqueness of row IDs. I searched for a function like this to no avail. Then I realized it doesn’t matter if the script doesn’t sort the table in the alphabetical order I expect as long as it applies the same “incorrect” sort to both the source and target tables _equally_. This means both tables will still be sorted in the same order just not in the order we expect from a typical sort in Google Sheets or Excel.

## Syncing Google Sheets to a Coda doc

After writing the [Coda -> Google Sheets](https://coda.io/d/How-to-sync-data-from-Coda-to-Google-Sheets-and-vice-versa-with-_dv4i9X8bdFe/Coda-Google-Sheets_suhDq) script, I thought the [Google Sheets -> Coda](https://coda.io/d/How-to-sync-data-from-Coda-to-Google-Sheets-and-vice-versa-with-_dv4i9X8bdFe/Google-Sheets-Coda_suKcq) script would be a breeze since I had written all the functions to convert and sort data. All I would have to do is just switch around some variables and everything would work out just fine. Turns out I was completely wrong since there are a bunch of edge cases to account for in Google Sheets that makes the sync a little more difficult compared to Coda to Google Sheets.

You can follow most of the [steps](https://coda.io/d/How-to-sync-data-from-Coda-to-Google-Sheets-and-vice-versa-with-_dv4i9X8bdFe/How-to-sync-data-from-Coda-to-Google-Sheets-and-vice-versa-with-_su8ir) in the Coda to Google Sheets setup to get the values you need for the script to run, but there are a few caveats and extra options you can set to get similar functionality as the [Coda -> Google Sheets](https://coda.io/d/How-to-sync-data-from-Coda-to-Google-Sheets-and-vice-versa-with-_dv4i9X8bdFe/Coda-Google-Sheets_suhDq) script:

- **Target Row Id Column**  — This is a key that stores each row’s unique ID from a Coda table. This value is in the “source row URL” (last 6 characters). Be default this variable is set as “Coda Row ID,” so make sure you don’t have a column in your Coda table with this name.
- **Do Not Delete Column**  — Unlike Google Sheets, the script is not written in a way where you can add additional rows to the _target_ Coda table without having them deleted when the sync runs. As mentioned [above](https://coda.io/d/How-to-sync-data-from-Coda-to-Google-Sheets-and-vice-versa-with-_dv4i9X8bdFe/How-to-sync-data-from-Coda-to-Google-Sheets-and-vice-versa-with-_su8ir) for the [Coda -> Google Sheets](https://coda.io/d/How-to-sync-data-from-Coda-to-Google-Sheets-and-vice-versa-with-_dv4i9X8bdFe/Coda-Google-Sheets_suhDq) script, you can add rows to your _target_ Google Sheet and not have them deleted on the sync. You need to create a checkbox column in your Coda table called `Do not delete` and check off the box for that row if you don’t want it to get deleted on the sync. If you prefer a different column name, just change the value for the `DO_NOT_DELETE_COLUMN` variable.
- **Rewrite Coda Table**  — Unlike the [Coda -> Google Sheets](https://coda.io/d/How-to-sync-data-from-Coda-to-Google-Sheets-and-vice-versa-with-_dv4i9X8bdFe/Coda-Google-Sheets_suhDq) script, you have the option to completely delete all the rows in your _target_ table and re-write them with all the rows from your _source_ Google Sheet. Set the `REWRITE_CODA_TABLE` variable to true if you want this behavior (may result in a faster sync).

### Column and row limitations

If you have edit access to the Google Sheet, you will need to add a column at the end of your table called something like “Source Row URL” similar to the “Coda Source Row URL” pattern mentioned [above](https://coda.io/d/How-to-sync-data-from-Coda-to-Google-Sheets-and-vice-versa-with-_dv4i9X8bdFe/How-to-sync-data-from-Coda-to-Google-Sheets-and-vice-versa-with-_su8ir) for the [Coda -> Google Sheets](https://coda.io/d/How-to-sync-data-from-Coda-to-Google-Sheets-and-vice-versa-with-_dv4i9X8bdFe/Coda-Google-Sheets_suhDq) script. After writing the data from Google Sheets to Coda for the first time, the unique row URLs from Coda are copied over into this “Source Row URL” column in your editable Google Sheets. Obviously this doesn’t apply to Google Sheets where you only have read-only access (more on that later).

One limitation of the script is that if you add a new column to the Google Sheet, you also need to add that same column name to the Coda table. It’s ok if the column _order_ isn’t the same in Coda, but that column name just needs to exist somewhere in the Coda table. You can just hide the column in Coda to make the table nice and clean. This is actually a limitation caused by the way I structured the script, so hopefully it doesn’t cause you too much inconvenience 😬.

![](https://cdn-images-1.medium.com/max/245/0*2uqYyCQU4uTcRy5A)

Be careful with empty rows in your data in Google Sheets because those rows also get “synced” over to Coda. Not only will those empty rows show up in your Coda table, they will get their own source URLs. Ideally, the Google Sheet won’t contain any empty rows and this won’t be a problem for you.

A few other “small” things:

1. **Formulas don’t sync**  — Probably not a huge surprise as this is also a limitation of the [Coda -> Google Sheets](https://coda.io/d/How-to-sync-data-from-Coda-to-Google-Sheets-and-vice-versa-with-_dv4i9X8bdFe/Coda-Google-Sheets_suhDq) script. Any columns with formulas you sync over to Coda will just be hard-coded to that column in your Coda table.
2. **Resetting column formats**  — When your Coda table is blank and you’re syncing over rows for the _first time_ from Google Sheets, you may have to change some of the column formats to the proper format. For instance, if your dates in Google Sheets are in [Zulu format](https://stackoverflow.com/questions/8405087/what-is-this-date-format-2011-08-12t201746-384z), Coda will sometimes interpret these values as a select list. After the sync, just change the column format in Coda to the Date format you want and future syncs will work just fine.
3. **You can’t sort your Google Sheet**  — The script looks for empty source row URLs in the `SOURCE_SHEET_SOURCE_ROW_COLUMN` in your Google Sheet and it scans that column until it finds an empty value to start pasting in new source row URLs from Coda. If you sort your table, that column will get all jumbled and the script will break. New rows that you add to the Google Sheet should have the source row URL column blank and these blank cells need to be contiguous.

### Setting a timer for source row URLs

You will notice that the data syncs over pretty quickly to Coda, but the `SOURCE_SHEET_SOURCE_ROW_COLUMN` (aka the “source row URL”) takes a couple seconds to show up in your Google Sheet. The reason this happens is because of the steps that need to happen for this sync to work:

1. Find the rows that need to be added from Google Sheets to Coda
2. Insert those new rows into Coda
3. Coda snapshots the new data added to your table
4. Look to see if the source row URLs have shown up in the Coda table
5. Copy over the source row URLs to Google Sheets once those URLs show up

The key step in #3 since that snapshot can take a few seconds to happen. If we try to copy the source row URLs right after the rows are inserted into the Coda table, the script will come up with nothing an no row URLs will show up in your Google Sheet.

To get around this, I added a little sleep timer to basically check for the source row URLs every two seconds:

```
while(currentCodaRows.length <= allRows['targetRows'].length) {
  timer += 2;
  if (timer == 60) { break; }
  Utilities.sleep(2000);
  currentCodaRows = retrieveRows();    
}
```

The `allRows[‘targetRows’]` object contains all the rows in your Coda table when the script runs for the first time. Every two seconds, the loop retrieves the rows in the Coda table in hopes that the the number of `currentCodaRows` has _exceeded_ the number of original rows when the script first ran. The loop also breaks after 30 seconds if, for some reason, the Coda API cannot retrieve all the number of current rows added to the table. So far it hasn’t taken more than five seconds for the URLs to show up, but this is on a small data set of a 5–10 rows being added each time I tested the script.

![](https://cdn-images-1.medium.com/max/500/0*zY-I7QE0DFkc_cHA)

This sure seems like a heck a lot of work just to added some new rows to a table in Coda. That’s why I put in a `REWRITE_CODA_TABLE` [variable](https://github.com/albertc44/coda-google-apps-script/blob/master/sheets_to_coda.js#L23) to override all this source row URLs business.

### Deleting and re-writing rows each time

As discussed with [updating rows](https://coda.io/d/How-to-sync-data-from-Coda-to-Google-Sheets-and-vice-versa-with-_dv4i9X8bdFe/How-to-sync-data-from-Coda-to-Google-Sheets-and-vice-versa-with-_su8ir) in the [Coda -> Google Sheets](https://coda.io/d/How-to-sync-data-from-Coda-to-Google-Sheets-and-vice-versa-with-_dv4i9X8bdFe/Coda-Google-Sheets_suhDq) script, I wanted to avoid this pattern of syncing data:

1. Delete all rows in the target
2. Copy all the rows from the source
3. Insert the copied rows into the blank target table

It didn’t seem like the right solution especially for a large table of thousands of rows because if you’re only changing or adding a few rows, the script has to delete and re-add all these thousands of rows. The simplicity of this approach is tempting, nonetheless. Just like the [Coda -> Google Sheets](https://coda.io/d/How-to-sync-data-from-Coda-to-Google-Sheets-and-vice-versa-with-_dv4i9X8bdFe/Coda-Google-Sheets_suhDq) script, the she[Google Sheets -> Coda](https://coda.io/d/How-to-sync-data-from-Coda-to-Google-Sheets-and-vice-versa-with-_dv4i9X8bdFe/Google-Sheets-Coda_suKcq) script is broken down into `addDeleteToCoda()` and `updateCoda()` functions. The former function adds and deletes rows while the latter updates any existing rows in Coda that may have changed in the source Google Sheet.

Blowing up the Coda table each time the sync runs would prevent the need for individual functions that add, delete, and update because the nature of blowing something up is that you can re-build from scratch. I haven’t measured which option is more performant but my hunch is that for smaller tables of data, setting `REWRITE_CODA_TABLE` to `true` may actually make the script run faster at the expense of not having the source URLs in your Google Sheet.

The `REWRITE_CODA_TABLE` option is actually important for Google Sheets files you only have _read-only access_ to. By default, you can’t write source row URLs to a Google Sheet you have view-access to, so there’s no point in using source row URLs to figure out which rows need to be added, deleted, and updated. **Side note:** the script doesn’t work on Google Sheets that have been [published to the web](https://support.google.com/docs/answer/183965?co=GENIE.Platform%3DDesktop&hl=en). You’ll know the Google Sheet is published when the URL has a `2PACX` in the URL like so:

![](https://cdn-images-1.medium.com/max/423/0*QXdI8_xV0loWMQzp)

### Getting permissions from Google Sheets

Instead of having to remember if you need to switch the `REWRITE_CODA_TABLE` variable to `true` when you’re syncing from a read-only Google Sheet, I did a little hack to get the permissions you have on the Google Sheet by trying to add the logged in user (you) as an editor to the Google Sheet:

```
function sheetsPermissions() {
  try {
    fromSpreadsheet.addEditor(Session.getActiveUser());
  } 
  catch (e) {
    REWRITE_CODA_TABLE = true; // If no access automatically rewrite Coda tables each sync
  }
}
```

If you have _edit-access_ to the Google Sheet, nothing happens since you are already an editor. If there is an error, then that means you don’t have permissions to add yourself as an editor to the Google Sheet (which means you only have read-only access). Int this case, the `REWRITE_CODA_TABLE` is set to `true` and the script goes on and blows up the Coda table and replaces it brand new with data from your Google Sheet.

## Final Caveats & Notes

There are many other variables to consider before implementing these scripts into your daily business-critical processes, but I think the given feature set should get you 90% of the way there. Having said that, there are a few more things to think about and small limitations about the scripts in general I’ve discovered along the way. This is by no means an exhaustive list.

### Using simple triggers in Google Apps Script

I thought that the [Google Sheets -> Coda](https://coda.io/d/How-to-sync-data-from-Coda-to-Google-Sheets-and-vice-versa-with-_dv4i9X8bdFe/Google-Sheets-Coda_suKcq) script could take advantage of [simple triggers](https://developers.google.com/apps-script/guides/triggers) to fire off the script. Basically you could have the script fire right when you make an edit to any cell, the moment the Google Sheet loads, etc. Unfortunately, there are a few [restrictions](https://developers.google.com/apps-script/guides/triggers#restrictions) to using simple triggers, and it looks like the script has to be entirely contained in Google Sheets to utilize simple triggers. Additionally, I don’t think the script could keep up with the speed of edits if you are looking for near real-time syncing. Data would just get choked as the script waits for source row URLs to appear and data would start pouring into your Coda doc.

![](https://cdn-images-1.medium.com/max/498/0*oYx95VUI6JdXhQih)

### Rate limits

There are rate limits for [Google Apps Script](https://developers.google.com/apps-script/guides/services/quotas) as well as [Coda](https://help.coda.io/en/articles/3370370-are-there-any-size-limitations-on-docs-accessible-via-the-api). I’ve tried syncing tables with 10,000 rows in both scripts (6 columns) and they both seem to work. I think in one test the [Google Sheets -> Coda](https://coda.io/d/How-to-sync-data-from-Coda-to-Google-Sheets-and-vice-versa-with-_dv4i9X8bdFe/Google-Sheets-Coda_suKcq) sync resulted in some rows missing in the Coda table. For the first time you sync data over, I’d recommend just doing a regular copy and paste instead of relying on the sync to copy all the data over correctly. Most likely, subsequent additions and edits would be as large so the sync should run smoothly.

### V8 runtime

If you have existing Google App Scripts, you may have noticed this fun error message at the top of your editor:

![](https://cdn-images-1.medium.com/max/526/0*8azmCw5nLtC0HoqX)

These scripts utilize the [V8 runtime](https://developers.google.com/apps-script/guides/v8-runtime) which takes advantage of a bunch of modern JavaScript features. The only changes I needed to make to upgrade the scripts was changing the syntax for `for each` [loops](https://developers.google.com/apps-script/guides/v8-runtime/migration#avoid_for_eachvariable_in_object).

### Moving off Coda or Google Sheets to a dedicated database

It’s tempting to use a Google Sheet or Coda doc as your de facto database. The interface is familiar, easy to edit and use, and it lives in your browser. The danger is when it feels _so convenient_ that you start putting thousands or hundreds of thousands of rows into your spreadsheet and maybe rely on Zapier or these Google Apps Scripts to sync data in and out of other applications you use every day to get work done.

If the process isn’t business-critical and your team can put up with this annoying little thing:

![](https://cdn-images-1.medium.com/max/265/0*XW_WhsnQ-wZYEzx-)<figcaption><em>Source: Ben Collins</em></figcaption>

…then by all means continue doing what you’re doing and pass the Google Sheet to the next intern or analyst who has to put up with updating it in the future. I would consider migrating your data to a dedicated database platform (like [Google BigQuery](https://cloud.google.com/bigquery)) which has a nice integration with Google Sheets. Lots more to say about this subject, but I’ll just leave it at that.

## Not a programmer

Most of this post is me pretending to know what I’m talking about. I’m not a programmer, and the scripts could probably be improved 10X by someone who actually knows what they’re doing and understands how algorithms work. There are unnecessary loops and bugs stamped all over the scripts so please proceed with caution ⛔️. If you happen to be someone who knows more about this stuff than me, consider [contributing](https://github.com/albertc44/coda-google-apps-script) to the code. I just did the bare minimum to get something to work and hopefully these scripts will be sufficient to get you on you your merry way of not having to copy and paste between Coda and Google Sheets 🤙.

![](https://cdn-images-1.medium.com/max/336/0*ggC9Nz1uVloUf5fJ)

* * *