For Each Iterator

The forEach node iterates over any array from upstream node results and spawns one child workflow per item — no external data source required.

How it works

A forEach node evaluates a JSONata expression at runtime and iterates over the resulting array. For each item, Graph Compose spawns a Temporal child workflow that executes the downstream nodes independently and in parallel.

graph LR A["HTTP Node (fetches data)"] --> B["forEach Node (iterates array)"] B --> C["Child Workflow 0"] B --> D["Child Workflow 1"] B --> E["Child Workflow N"]
  1. Upstream node executes and returns data containing an array
  2. forEach node evaluates its items expression against the workflow state
  3. Child workflows are spawned — one per array item — each executing the downstream nodes
  4. Parent workflow aggregates results and continues

Unlike source_iterator nodes (which call an external service to read from Google Sheets), forEach operates entirely in-memory against workflow results. No HTTP call is needed for the iteration itself.

Schema

A forEach node requires two fields beyond the base node properties: a forEach.items expression and at least one dependency.

import { GraphCompose } from '@graph-compose/client'

const graph = new GraphCompose({
  token: process.env.GRAPH_COMPOSE_TOKEN
})

graph
  .node("get_users")
    .get("https://api.example.com/users")
  .end()

  .forEach("process_each_user", {
    items: "{{ results.get_users.data.users }}",
    dependencies: ["get_users"]
  })
FieldTypeRequiredDescription
type"forEach"YesIdentifies this as a forEach node.
forEach.itemsstringYesA JSONata expression wrapped in {{ }} that evaluates to an array.
dependenciesstring[]YesAt least one upstream node ID. Required so the items expression has data to evaluate against.

Child workflow context

Each child workflow receives the current item and its index. Use these template expressions inside downstream nodes:

ExpressionDescriptionExample value
{{ row.data }}The current array item{ "id": 1, "email": "alice@example.com" }
{{ row.data.<property> }}A nested property on the item"alice@example.com"
{{ row.index }}Zero-based iteration index0, 1, 2, ...
{{ context.<key> }}Global workflow contextValue passed at execution time
{{ results.<nodeId>.data.<field> }}Output from upstream nodes within the childDepends on node output

Basic example

This workflow fetches a list of users from an API, then sends a personalized email to each one.

import { GraphCompose } from '@graph-compose/client'

const graph = new GraphCompose({
  token: process.env.GRAPH_COMPOSE_TOKEN
})

graph
  .node("get_users")
    .get("https://api.example.com/users")
  .end()

  .forEach("for_each_user", {
    items: "{{ results.get_users.data.users }}",
    dependencies: ["get_users"]
  })

  .node("send_email")
    .post("https://api.sendgrid.com/v3/mail/send")
    .withHeaders({
      "Authorization": "Bearer {{ $secret('sendgrid_key') }}"
    })
    .withBody({
      to: "{{ row.data.email }}",
      subject: "Hello {{ row.data.name }}!",
      text: "You are user #{{ row.index }}"
    })
    .withDependencies(["for_each_user"])
  .end()

If the API returns three users, Graph Compose spawns three child workflows. Each child executes send_email with access to {{ row.data.email }}, {{ row.data.name }}, and {{ row.index }}.

Filtering with JSONata

You can filter arrays before iteration using JSONata expressions. Only items matching the filter are iterated:

Filter active users

{
  "id": "for_each_active_user",
  "type": "forEach",
  "forEach": {
    "items": "{{ results.get_users.data.users[active = true] }}"
  },
  "dependencies": ["get_users"]
}

More JSONata filter examples:

ExpressionFilters to
{{ results.node.data.items[price > 100] }}Items with price above 100
{{ results.node.data.orders[status = "pending"] }}Orders with pending status
{{ results.node.data.users[age >= 18 and country = "US"] }}US adults

See JSONata Deep Dive for the full expression reference.

Primitive arrays

The items expression can evaluate to an array of primitives (strings, numbers) rather than objects. Access each value directly via {{ row.data }}:

Iterate over strings

{
  "nodes": [
    {
      "id": "get_tags",
      "type": "http",
      "http": {
        "method": "GET",
        "url": "https://api.example.com/tags"
      }
    },
    {
      "id": "for_each_tag",
      "type": "forEach",
      "forEach": {
        "items": "{{ results.get_tags.data.tags }}"
      },
      "dependencies": ["get_tags"]
    },
    {
      "id": "process_tag",
      "type": "http",
      "dependencies": ["for_each_tag"],
      "http": {
        "method": "POST",
        "url": "https://api.example.com/tags/process",
        "body": {
          "tag": "{{ row.data }}",
          "index": "{{ row.index }}"
        }
      }
    }
  ]
}

If tags is ["frontend", "backend", "devops"], three child workflows are spawned. In child 0, {{ row.data }} resolves to "frontend" and {{ row.index }} resolves to 0.

Result aggregation

After all child workflows complete, the parent forEach node records aggregated results:

FieldTypeDescription
totalItemsnumberTotal number of items that were iterated
completedItemsnumber[]Indices of successfully completed items
failedItemsnumber[]Indices of items whose child workflows failed

You can inspect these through the Get Workflow API in the node's execution state. This is useful for monitoring and debugging batch operations.

Comparison with source iterator

Graph Compose offers two iteration mechanisms. Choose the one that fits your data source:

FeatureforEachsource_iterator
Data sourceAny array from workflow resultsExternal data source (Google Sheets)
HTTP call requiredNo (in-memory)Yes (calls io-nodes service)
DependenciesAt least one requiredNone (runs first)
SetupJSONata expressionGoogle OAuth + spreadsheet selection
Best forAPI responses, computed arraysSpreadsheet-driven workflows
Row access{{ row.data }}, {{ row.index }}{{ row.data.column }}, {{ row.index }}
Destination node supportNoYes (writes back to Sheets)

When to use forEach: Your array comes from an API call or is computed within the workflow. No external integrations needed.

When to use source_iterator: Your data lives in Google Sheets and you want to read rows, process them, and optionally write results back to a sheet.

Edge cases

Empty arrays

If the items expression evaluates to an empty array, no child workflows are spawned. The forEach node completes immediately with totalItems: 0 and the parent workflow continues to any nodes that don't depend on the forEach's children.

Non-array results

If the expression evaluates to null, undefined, or a non-array value, the node fails with a clear error message. Make sure your expression always resolves to an array.

Large arrays

Each item spawns a separate Temporal child workflow. For arrays under 100 items, performance is excellent. For arrays over 1,000 items, consider:

  • Filtering the array with JSONata before iteration
  • Using source_iterator with pagination for very large datasets
  • Batching items in your upstream API

Nested forEach

A forEach child workflow can itself contain a forEach node, creating nested iteration. Use this with caution — the number of child workflows grows multiplicatively. For example, iterating over 100 items with each child iterating over 50 sub-items creates 5,000 child workflows.

Next steps