Skip to content
Expedify
5 min

Google Sheets

The node that gets your data in front of people who will never open Expedify. Eleven operations, four of which turn a sheet into something you can update rather than only append to.

Some of what a workflow produces has to leave the product. A finance team that works in spreadsheets, a partner who wants a weekly list, a colleague who will not be given a login for one report a month. A sheet is where that lands.

The obvious use is appending: something happened, add a row. That works and it is what most people build. The more useful half of this node is the four operations that match on a value first — because a sheet you can update is a shared record, and a sheet you can only append to is a log that gets stale the moment anything changes.

Eleven operations, in four groups

Operations
read_range · lookup_rows · list_spreadsheets
What they are for
Pull data back out. lookup_rows finds by value rather than by position.

Operations
write_range · append_rows · clear_range
What they are for
Put data at a place you name. Append is the one everybody starts with.

Operations
update_matching_rows · append_or_update · delete_matching_rows
What they are for
Find rows by a key and change them. This is where the node stops being a log.

Operations
create_spreadsheet · add_sheet
What they are for
Make the file or the tab before you write to it.

operation

What it holds
Google Sheets operation to perform One of: read_range · write_range · append_rows · create_spreadsheet · add_sheet · clear_range · list_spreadsheets · lookup_rows · update_matching_rows · append_or_update · delete_matching_rows Defaults to read_range.

spreadsheet_config

What it holds

data_config

What it holds

match_config

What it holds
Row-matching config for lookup_rows, update_matching_rows, append_or_update and delete_matching_rows (VLOOKUP-style: find rows by a column value, then read/update/delete them).

sheet_name

What it holds
Sheet tab to operate on (chosen via the tab selector). Used by range and match operations. Defaults to Sheet1.

Those config objects group what the operation needs. spreadsheet_config says which file and where in it. data_config carries what you are writing. match_config is the one that matters for the third group — which column is the key, what value to look for, and what to do when several rows match.

A worked example

One row per contact, kept current. The contact changes in the CRM and the sheet follows — updating the existing row if the email is already there, adding one if it is not.

append_or_update, keyed on email

Two nodes, and the whole idea is in match_config.

Read match_config first. match_column is Email — the header of the column, not a letter. That is the VLOOKUP key, and it is why the sheet's first row has to hold real headers and header_row has to point at it. Get that wrong and every match fails silently, because a key that matches nothing is indistinguishable from a contact you have not seen before.

match_mode is set to case-insensitive here, which for email addresses is the only correct answer — people type their address in whatever case they like and exact would create a duplicate row for each variant. match_position decides what happens when several rows match; first is the safe choice while you are learning, because all will happily rewrite twenty rows.

The values are a stringified two-dimensional array. This is the node's own warning and it is the thing people get wrong on their first attempt. Not an object, not a list of fields — a JSON string containing an array of rows, each row an array of cells, in column order. One row is still an array inside an array.

The setting that quietly corrupts data

value_input_option defaults to USER_ENTERED, which means Sheets interprets what you write exactly as if a person had typed it. A phone number beginning with a plus becomes a formula and errors. An order reference like 1/2 becomes a date. A leading zero on a postcode disappears. A long id becomes scientific notation. None of that raises anything — the write succeeds and the value in the cell is not the value you sent.

Set it to RAW for anything that is an identifier rather than a number: phone numbers, references, postcodes, ids, product codes. The workflow above uses RAW for exactly this reason. Use USER_ENTERED deliberately, when you want a date to be a date or a number to be summable — not by default.

What comes back

{{alias.data}}

What you get
The rows a read or lookup returned. An array of arrays, unless headers were included.

{{alias.row_count}}

What you get
How many rows were read, written or matched. The number to check when you suspect a match found nothing.

{{alias.spreadsheet_id}}

What you get
Useful after create_spreadsheet — it is the only place the new file's id appears.

What breaks

Watch out: operation is an enum (read_range/write_range/append_rows/…); write values go in data_config.values as a stringified 2-D array.

A failed match is not an error. Ask update_matching_rows to update a row that is not there and nothing happens, successfully. append_or_update is usually what you meant — it writes the row either way — but if you genuinely want an update-only operation, read {{alias.row_count}} and branch on zero.

Sheets are not a database and this node cannot make them one. Two runs matching on the same key at the same moment will both find the row and both write to it, and there is nothing here to prevent that. For a sheet updated a few times an hour it does not matter. For a high-frequency sync it does, and the answer is to write to your CRM and export on a schedule instead.

The sheet's shape is a contract nobody enforces. Somebody inserts a column on Monday and every write_range built on positions is now writing into the wrong cells — silently, because a range is just coordinates. The match-based operations survive this, since they work from header names. That is the strongest practical argument for using them.

Try it

  1. Make a sheet with headers in row 1 and connect a Google account. Run read_range over A1:E10 and look at the shape of {{alias.data}} — arrays inside an array.
  2. Append one row with append_rows. Get the stringified 2-D array right; this is the step that fails first.
  3. Write a phone number starting with + using the default USER_ENTERED, then the same number with RAW. Compare the two cells. This is the lesson.
  4. Switch to append_or_update keyed on an email column, run it twice with the same contact, and confirm you have one row rather than two.
  5. Now change that contact's status in the CRM and let the workflow fire. The row should change in place.

Next: Google Docs — the same integration family when the output is prose rather than rows, and somebody has to read it.

Related lessons