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.

Visual workflow builder showing a source node, HTTP processing node, and destination node connected in sequence.

How it works

A Google Sheets workflow has three phases:

  1. 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.
  2. 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.
  3. Write: If a destination node is present, each child workflow writes its results back to the target sheet at the correct row position.
graph LR A["Source Node (reads rows)"] --> B["Process Nodes (one child per row)"] B --> C["Destination Node (writes results)"]

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 NameEmailPlan
Row 1 (headers)Customer NameEmailPlan
Row 2 (data)Alicealice@example.compro
Row 3 (data)Bobbob@example.comfree
Row 4 (data)Carolcarol@example.compro

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.

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 headerNormalized keyTemplate expression
Customer Namecustomer_name{{ row.data.customer_name }}
Email Addressemail_address{{ row.data.email_address }}
2024 Revenue2024_revenue{{ row.data.2024_revenue }}
Phone #phone{{ row.data.phone }}

The normalization rules:

  1. Trim leading and trailing whitespace
  2. Convert to lowercase
  3. Remove special characters
  4. Replace spaces and hyphens with underscores
  5. 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:

SourceSyntaxDescription
Row datarow.data.column_nameThe current row's column values (snake_case keys)
Row indexrow.indexZero-based iteration index
Contextcontext.keyGlobal data passed when executing the workflow
Node resultsresults.nodeId.data.fieldOutput 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 expressionColumn header writtenResolved value
{{ row.data.customer_name }}row.data.customer_nameThe original row value
{{ results.enrich_customer.data.score }}results.enrich_customer.data.scoreThe enrichment score from the upstream node
{{ results.enrich_customer.data.verified }}results.enrich_customer.data.verifiedBoolean 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.

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 mappingWrites
{{ 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.

Next steps