You have a rolling KPI tracker in Google Sheets. Every Monday you add a row: date, channel, revenue, status. Right now you open a browser, scroll to the bottom, click an empty cell, type four values, hit save. What if that was one Python call instead?
read_range yesterday pulled the whole sheet back as a list of rows. So writing back is probably just the reverse — pass a list and it lands in the sheet?
Exactly right. The action is GOOGLESHEETS_SPREADSHEETS_VALUES_APPEND. You give it the spreadsheet ID, a range like "A1:Z1" to tell Sheets where to look for the last row, and a values key that wraps your row in an outer list. Here's the shape:
result = toolset.execute_action(Action.GOOGLESHEETS_SPREADSHEETS_VALUES_APPEND, {
"spreadsheet_id": sheet_id,
"range": range_a1,
"values": [values],
})Why does values get wrapped in another list? I'm passing a list already — why [values] instead of just values?
The API expects a grid — rows of rows. Even when you're appending a single row, it needs to arrive as a list of rows: [["2026-04-12", "Paid Search", 12400, "on target"]]. Your outer list is the grid; your inner list is the row. Pass values flat and the action will quietly fail or misplace columns.
So a row in Sheets is just a list. I could loop over 52 weeks of data and append them all in one block. That's my whole annual KPI history loaded in seconds.
You just described a full year of weekly snapshots in two sentences. And yes — a for loop over a list of rows, one append_row call each, and your tracker is complete. One rule before you point this at a shared Sheet:
def append_row(sheet_id: str, range_a1: str, values: list) -> dict:
result = toolset.execute_action(Action.GOOGLESHEETS_SPREADSHEETS_VALUES_APPEND, {
"spreadsheet_id": sheet_id,
"range": range_a1,
"values": [values],
})
return resultWhat's the rule?
Appending is irreversible in the sense that there is no built-in undo. In real workflows, always confirm the row values look correct before you call append_row — log them, print them, show a preview. Tests should target a personal or throwaway Sheet, not a shared team tracker. Treat every write action like an email send: confirm first, append once.
GOOGLESHEETS_SPREADSHEETS_VALUES_APPEND finds the first empty row after the specified range and writes your values there.
The values parameter expects a grid (list of rows), not a single row:
| What you have | What to pass |
|---|---|
One row: ["date", "ch", 100] | [["date", "ch", 100]] |
| Multiple rows | [[row1], [row2]] |
Passing values flat silently misroutes columns.
Always confirm values before appending to a shared Sheet. In production workflows: build the row, log it, get approval, then append. Tests should target a personal or throwaway spreadsheet.
You have a rolling KPI tracker in Google Sheets. Every Monday you add a row: date, channel, revenue, status. Right now you open a browser, scroll to the bottom, click an empty cell, type four values, hit save. What if that was one Python call instead?
read_range yesterday pulled the whole sheet back as a list of rows. So writing back is probably just the reverse — pass a list and it lands in the sheet?
Exactly right. The action is GOOGLESHEETS_SPREADSHEETS_VALUES_APPEND. You give it the spreadsheet ID, a range like "A1:Z1" to tell Sheets where to look for the last row, and a values key that wraps your row in an outer list. Here's the shape:
result = toolset.execute_action(Action.GOOGLESHEETS_SPREADSHEETS_VALUES_APPEND, {
"spreadsheet_id": sheet_id,
"range": range_a1,
"values": [values],
})Why does values get wrapped in another list? I'm passing a list already — why [values] instead of just values?
The API expects a grid — rows of rows. Even when you're appending a single row, it needs to arrive as a list of rows: [["2026-04-12", "Paid Search", 12400, "on target"]]. Your outer list is the grid; your inner list is the row. Pass values flat and the action will quietly fail or misplace columns.
So a row in Sheets is just a list. I could loop over 52 weeks of data and append them all in one block. That's my whole annual KPI history loaded in seconds.
You just described a full year of weekly snapshots in two sentences. And yes — a for loop over a list of rows, one append_row call each, and your tracker is complete. One rule before you point this at a shared Sheet:
def append_row(sheet_id: str, range_a1: str, values: list) -> dict:
result = toolset.execute_action(Action.GOOGLESHEETS_SPREADSHEETS_VALUES_APPEND, {
"spreadsheet_id": sheet_id,
"range": range_a1,
"values": [values],
})
return resultWhat's the rule?
Appending is irreversible in the sense that there is no built-in undo. In real workflows, always confirm the row values look correct before you call append_row — log them, print them, show a preview. Tests should target a personal or throwaway Sheet, not a shared team tracker. Treat every write action like an email send: confirm first, append once.
GOOGLESHEETS_SPREADSHEETS_VALUES_APPEND finds the first empty row after the specified range and writes your values there.
The values parameter expects a grid (list of rows), not a single row:
| What you have | What to pass |
|---|---|
One row: ["date", "ch", 100] | [["date", "ch", 100]] |
| Multiple rows | [[row1], [row2]] |
Passing values flat silently misroutes columns.
Always confirm values before appending to a shared Sheet. In production workflows: build the row, log it, get approval, then append. Tests should target a personal or throwaway spreadsheet.
Create a free account to get started. Paid plans unlock all tracks.