Node Types
Each node in a workflow is a unit of work. This page covers all node types and the configuration options available to them.
HTTP nodes
HTTP nodes make requests to endpoints you control. You define the method, URL, headers, and body. Graph Compose calls your endpoint and stores the response for downstream nodes to reference via template expressions.
Supported HTTP methods: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS.
import { GraphCompose } from '@graph-compose/client'
const graph = new GraphCompose({
token: process.env.GRAPH_COMPOSE_TOKEN
})
graph
.node("get_user")
.get("https://api.example.com/users/{{ context.userId }}")
.withHeaders({
"Authorization": "Bearer {{ $secret('api_token') }}"
})
.end()
Template expressions inside URLs, headers, and bodies are resolved at runtime. Use {{ context.key }} for input data, {{ results.nodeId.data.field }} for upstream node responses, and {{ $secret('name') }} for credentials. See Template Syntax for the full reference.
Common configuration
These options apply to all node types that execute work (HTTP, error boundary, iterator, destination, and ADK nodes).
Dependencies
The dependencies array controls execution order. A node waits for all its dependencies to complete before running. Nodes with no dependency relationship run in parallel automatically.
Dependencies
graph
.node("check_inventory")
.post("https://api.example.com/inventory/check")
.withBody({
items: "{{ results.get_order.data.items }}"
})
.withDependencies(["get_order"])
.end()
Dependencies (JSON)
{
"id": "check_inventory",
"type": "http",
"dependencies": ["get_order"],
"http": {
"method": "POST",
"url": "https://api.example.com/inventory/check",
"body": {
"items": "{{ results.get_order.data.items }}"
}
}
}
Retry policy
Configure automatic retries with exponential backoff via activityConfig.retryPolicy.
Retry policy
graph
.node("call_api")
.get("https://api.example.com/data")
.withRetries({
maximumAttempts: 5,
initialInterval: "1s",
backoffCoefficient: 2,
maximumInterval: "30s"
})
.end()
Retry policy (JSON)
{
"id": "call_api",
"type": "http",
"http": {
"method": "GET",
"url": "https://api.example.com/data"
},
"activityConfig": {
"retryPolicy": {
"maximumAttempts": 5,
"initialInterval": "1s",
"backoffCoefficient": 2,
"maximumInterval": "30s"
}
}
}
| Field | Description |
|---|---|
maximumAttempts | Total number of attempts before the node is marked as failed. Default: 1 (no retries). |
initialInterval | Time before the first retry. Accepts duration strings: "1s", "500ms", "2m". |
backoffCoefficient | Multiplier applied to each subsequent retry interval. Minimum: 1. |
maximumInterval | Cap on the retry interval. Prevents unbounded backoff. |
Timeouts
Two timeout options control how long a node can run:
startToCloseTimeout: Maximum time for a single execution attempt. If the attempt exceeds this duration, it is retried (if retries remain) or marked as failed.scheduleToCloseTimeout: Maximum total time from when the node is scheduled until it completes, including all retry attempts.
Timeouts
graph
.node("slow_api")
.post("https://api.example.com/process")
.withStartToCloseTimeout("30s")
.withScheduleToCloseTimeout("5m")
.end()
Timeouts (JSON)
{
"id": "slow_api",
"type": "http",
"http": {
"method": "POST",
"url": "https://api.example.com/process"
},
"activityConfig": {
"startToCloseTimeout": "30s",
"scheduleToCloseTimeout": "5m"
}
}
Conditions
Nodes can include conditional logic to control workflow execution. Three condition types are available:
continueTo: Route execution to specific nodes based on the current node's result.terminateWhen: End the workflow when an expression evaluates to true.pollUntil: Re-execute the node until an expression evaluates to true.
Conditions
graph
.node('check_status')
.get('https://api.example.com/status')
.withConditions({
continueTo: [
{ to: 'process_data', when: '{{ results.check_status.data.state = "ready" }}' }
],
terminateWhen: ['{{ results.check_status.data.state = "cancelled" }}'],
pollUntil: ['{{ results.check_status.data.complete = true }}']
})
.end()
Error boundary nodes
An error boundary wraps one or more nodes and catches failures. When a protected node fails, Graph Compose calls the error boundary's HTTP endpoint with the failure context, then continues executing the rest of the workflow.
graph
.errorBoundary("payment_rollback", ["process_payment"])
.post("https://api.example.com/payments/rollback")
.withBody({
orderId: "{{ context.orderId }}"
})
.end()
Your error boundary endpoint receives an errorContext object in the request body containing the failure details from the protected node.
ForEach nodes
A forEach node evaluates a JSONata expression against upstream results and spawns one child workflow per item in the resulting array. Each child workflow has access to the current item via {{ row.data }} and the zero-based index via {{ row.index }}.
Unlike source iterators, forEach nodes operate entirely in-memory — no external HTTP call is needed. This makes them ideal for iterating over arrays returned by HTTP or ADK nodes.
Nodes that depend on the forEach run inside each child workflow. To run nodes after the loop completes (back in the parent workflow), add an endForEach node to close the loop — see EndForEach nodes.
graph
.node("get_users")
.get("https://api.example.com/users")
.end()
.forEach("for_each_user")
.withItems("{{ results.get_users.data.users }}")
.withDependencies(["get_users"])
.end()
.node("send_email")
.post("https://api.sendgrid.com/v3/mail/send")
.withBody({
to: "{{ row.data.email }}",
subject: "Hello {{ row.data.name }}!"
})
.withDependencies(["for_each_user"])
.end()
ForEach configuration
Optional settings are placed under a config object on the forEach node.
| Field | Type | Description |
|---|---|---|
config.continueOnError | boolean | When true, the forEach completes even if some children fail. Failed items are reported in failedItems[]. Overall status is 207 (partial success). Default: false. |
config.maxFailures | number | Abort after this many child failures, even with continueOnError enabled. |
config.concurrency | number | Maximum child workflows running simultaneously. Default: unbounded. |
config.childWorkflowConfig | object | Per-child timeout and retry policy. |
config.childWorkflowConfig.workflowExecutionTimeout | string | Maximum duration for each child workflow (e.g. "5m"). Defaults to the parent workflow's timeout. |
config.childWorkflowConfig.retry | object | Retry policy for failed child workflows (same shape as activityConfig.retryPolicy). |
graph
.forEach("process_users")
.withItems("{{ results.get_users.data.users }}")
.withDependencies(["get_users"])
.withConcurrency(5)
.withContinueOnError(true)
.withMaxFailures(10)
.withChildWorkflowConfig({
workflowExecutionTimeout: "5m"
})
.end()
Items expression behavior
The forEach.items expression must evaluate to an array at runtime. Here's what happens in edge cases:
| Scenario | Behavior |
|---|---|
| Evaluates to an array | Each element becomes a child workflow. Access via {{ row.data }}. |
Empty array [] | forEach completes immediately with totalItems: 0 and items: []. |
Evaluates to undefined | forEach completes immediately (treated as empty). |
| Evaluates to a non-array (string, object, number) | forEach fails with a type error. |
| Expression syntax error | forEach fails before spawning any children. |
ForEach nodes require at least one dependency so the items expression has upstream data to evaluate against. You can filter arrays using JSONata (e.g., {{ results.node.data.users[active = true] }}).
EndForEach nodes
An endForEach node marks the structural boundary of a forEach loop. It defines where the loop body ends and continuation begins:
- Nodes between the
forEachandendForEachrun inside each child workflow (once per item). - Nodes downstream of the
endForEachrun once in the parent workflow with access to the aggregated results.
The endForEach node is auto-completed by the engine. It doesn't execute any logic — it's purely a structural marker.
graph
.node("get_users")
.get("https://api.example.com/users")
.end()
.forEach("process_users")
.withItems("{{ results.get_users.data.users }}")
.withDependencies(["get_users"])
.withConcurrency(5)
.end()
// runs inside each child workflow
.node("enrich_user")
.post("https://api.example.com/enrich")
.withDependencies(["process_users"])
.withBody({
id: "{{ row.data.id }}",
email: "{{ row.data.email }}"
})
.end()
// close the loop
.endForEach("process_users_end")
.closes("process_users")
.withDependencies(["enrich_user"])
.end()
// continuation — runs once in parent
.node("send_summary")
.post("https://api.example.com/summary")
.withDependencies(["process_users_end"])
.withBody({
total: "{{ results.process_users.data.totalItems }}",
enriched: "{{ results.process_users.data.items }}"
})
.end()
Aggregated result shape
After all children complete, the forEach node's result is available to continuation nodes at results.<forEachId>.data:
Aggregated result
{
"totalItems": 3,
"successfulItems": 3,
"failedItems": [],
"items": [
{ "index": 0, "nodeId": "enrich_user", "data": { "enriched": true, "score": 92 } },
{ "index": 1, "nodeId": "enrich_user", "data": { "enriched": true, "score": 78 } },
{ "index": 2, "nodeId": "enrich_user", "data": { "enriched": true, "score": 85 } }
]
}
| Field | Description |
|---|---|
forEachId | The id of the forEach node this endForEach closes. Required. |
dependencies | Must reference the last node(s) inside the forEach loop. |
If you don't need continuation nodes (i.e., the forEach is the last step), you don't need an endForEach node. It's only required when you have nodes that should run after the loop completes.
Source iterator nodes
A source iterator reads rows from a data source (such as Google Sheets) and spawns one child workflow per row. Each child workflow has access to the current row via {{ row.data.columnName }} and the zero-based index via {{ row.index }}.
Source iterators cannot have dependencies. They always run at the start of the workflow.
Destination nodes
A destination node writes results back to a data destination such as Google Sheets. You define column mappings using a cellValues array, where each entry maps a column name to a value (which can include template expressions referencing upstream node results).
Confirmation nodes
A confirmation node pauses the workflow and waits for human approval before continuing. Downstream nodes remain blocked until the confirmation signal is received through the Graph Compose dashboard or API.
graph
.confirmation("approve_order")
.withDependencies(["review_order"])
.end()
Confirmation nodes do not require an HTTP configuration. They are a control flow mechanism that blocks execution until a signal is received.
ADK nodes
An ADK (Agent Development Kit) node runs a multi-agent AI workflow within a single Graph Compose node. An ADK node contains agent definitions (LLM, Sequential, Parallel, and Loop agents), tool configurations, and orchestration logic.
ADK nodes integrate with the rest of your workflow graph. They can depend on HTTP nodes, access context and upstream results, and have downstream nodes that depend on their output.