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.
- Upstream node executes and returns data containing an array
- forEach node evaluates its
itemsexpression against the workflow state - Child workflows are spawned — one per array item — each executing the downstream nodes
- 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"]
})
| Field | Type | Required | Description |
|---|---|---|---|
type | "forEach" | Yes | Identifies this as a forEach node. |
forEach.items | string | Yes | A JSONata expression wrapped in {{ }} that evaluates to an array. |
dependencies | string[] | Yes | At 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:
| Expression | Description | Example 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 index | 0, 1, 2, ... |
{{ context.<key> }} | Global workflow context | Value passed at execution time |
{{ results.<nodeId>.data.<field> }} | Output from upstream nodes within the child | Depends on node output |
The row.data and row.index interface is identical to source_iterator child workflows. If you are migrating from a Google Sheets-based iteration to an in-memory one, your downstream node expressions stay the same.
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:
| Expression | Filters 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:
| Field | Type | Description |
|---|---|---|
totalItems | number | Total number of items that were iterated |
completedItems | number[] | Indices of successfully completed items |
failedItems | number[] | 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:
| Feature | forEach | source_iterator |
|---|---|---|
| Data source | Any array from workflow results | External data source (Google Sheets) |
| HTTP call required | No (in-memory) | Yes (calls io-nodes service) |
| Dependencies | At least one required | None (runs first) |
| Setup | JSONata expression | Google OAuth + spreadsheet selection |
| Best for | API responses, computed arrays | Spreadsheet-driven workflows |
| Row access | {{ row.data }}, {{ row.index }} | {{ row.data.column }}, {{ row.index }} |
| Destination node support | No | Yes (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_iteratorwith 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.