Google Sheets Data Nodes
Source and destination nodes connect Google Sheets to your workflows. A source node reads rows from a spreadsheet and spawns one child workflow per row. A destination node writes results back to a sheet when each row finishes processing.
How it works
A Google Sheets workflow has three phases:
- Read: The source iterator node calls the io-nodes service, which reads row 1 as column headers and returns every subsequent row as an item.
- Process: Graph Compose spawns one Temporal child workflow per row. Each child workflow executes the downstream nodes independently and in parallel, with access to the current row's data via template expressions.
- Write: If a destination node is present, each child workflow writes its results back to the target sheet at the correct row position.
Structure your sheet
Row 1 of your spreadsheet must contain column headers. Graph Compose reads this row to determine field names and starts processing data from row 2 onward. Each subsequent row becomes one item in the iteration.
| Customer Name | Plan | ||
|---|---|---|---|
| Row 1 (headers) | Customer Name | Plan | |
| Row 2 (data) | Alice | alice@example.com | pro |
| Row 3 (data) | Bob | bob@example.com | free |
| Row 4 (data) | Carol | carol@example.com | pro |
In this example, Graph Compose spawns three child workflows, one for each data row. Empty cells are returned as null. Keep your headers in a single row and avoid merged cells.
Configure a source node
Source nodes are configured through the visual workflow builder at /dashboard. Drag a Data Source node onto the canvas, select Google as the source, and choose a spreadsheet from Google Drive.
You must connect a Google account before using data nodes. Go to /dashboard/connections to set up your OAuth connection.
Column normalization
The io-nodes service normalizes column headers from your sheet into snake_case keys. This makes them safe to use in template expressions regardless of how they are formatted in the spreadsheet.
| Original header | Normalized key | Template expression |
|---|---|---|
Customer Name | customer_name | {{ row.data.customer_name }} |
Email Address | email_address | {{ row.data.email_address }} |
2024 Revenue | 2024_revenue | {{ row.data.2024_revenue }} |
Phone # | phone | {{ row.data.phone }} |
The normalization rules:
- Trim leading and trailing whitespace
- Convert to lowercase
- Remove special characters
- Replace spaces and hyphens with underscores
- Remove leading and trailing underscores
If two columns produce the same normalized key after these rules, the source node returns a warning. Rename one of the columns in your sheet to avoid collisions.
Access row data in downstream nodes
Each child workflow receives three data sources for template expressions:
| Source | Syntax | Description |
|---|---|---|
| Row data | row.data.column_name | The current row's column values (snake_case keys) |
| Row index | row.index | Zero-based iteration index |
| Context | context.key | Global data passed when executing the workflow |
| Node results | results.nodeId.data.field | Output from completed upstream nodes in this child |
Here is an HTTP node that uses the current row's data to call an external API:
import { GraphCompose } from '@graph-compose/client'
const graph = new GraphCompose({
token: process.env.GRAPH_COMPOSE_TOKEN
})
graph
.node("enrich_customer")
.post("https://api.example.com/customers/enrich")
.withHeaders({
"Authorization": "Bearer {{ $secret('api_token') }}"
})
.withBody({
name: "{{ row.data.customer_name }}",
email: "{{ row.data.email }}",
index: "{{ row.index }}"
})
.end()
Configure a destination node
Destination nodes are also configured in the visual builder. Drag a Data Destination node onto the canvas, select Google, and choose the target spreadsheet.
Use the output mapping panel to define which values to write. Each mapping is a template expression. The column name is derived from the expression path, and the value is resolved at runtime.
| Template expression | Column header written | Resolved value |
|---|---|---|
{{ row.data.customer_name }} | row.data.customer_name | The original row value |
{{ results.enrich_customer.data.score }} | results.enrich_customer.data.score | The enrichment score from the upstream node |
{{ results.enrich_customer.data.verified }} | results.enrich_customer.data.verified | Boolean verification status |
If a column does not exist in the destination sheet, the io-nodes service creates it automatically by appending a new column header to row 1.
The destination node writes each result to the correct row automatically. You do not need to specify row positions.
Example workflow
This workflow reads a list of customers from a Google Sheet, verifies each customer's email through an external API, and writes the verification status back to a destination sheet.
The source sheet has columns: Customer Name, Email, Plan. The HTTP node sends each row's email to a verification endpoint:
Verify email node body
{
"email": "{{ row.data.email }}",
"customer": "{{ row.data.customer_name }}"
}
The destination node maps the results back using these output mappings:
| Output mapping | Writes |
|---|---|
{{ row.data.customer_name }} | The original customer name from the source row |
{{ row.data.email }} | The original email from the source row |
{{ results.verify_email.data.is_valid }} | Whether the email passed verification |
{{ results.verify_email.data.checked_at }} | Timestamp of the verification check |
If the source sheet has 500 rows, Graph Compose spawns 500 child workflows. Each runs independently, and the destination node writes each result to the corresponding row in the output sheet.
Constraints
- Source iterator nodes cannot have dependencies. They always run first.
- Source nodes can only connect to HTTP or ADK nodes (not directly to destination nodes).
- Destination nodes can only receive connections from HTTP or ADK nodes.
- Google Sheets connections are configured exclusively through the visual builder. The SDK and REST API do not support configuring OAuth-based sheet connections directly.