Core Concepts
A workflow is a JSON document describing a directed acyclic graph (DAG) of nodes. This page explains the concepts that make up that graph and how Graph Compose executes it.
The Workflow Graph
Every workflow is a JSON object with a nodes array, optional context, and optional configuration. The nodes and their dependencies form a DAG, which Graph Compose resolves to determine execution order.
Workflow Structure
{
"nodes": [],
"context": {
"orderId": "order_123",
"region": "us-east"
},
"webhookUrl": "https://api.example.com/webhook",
"workflowConfig": {
"workflowExecutionTimeout": "5 minutes"
}
}
| Field | Required | Description |
|---|---|---|
nodes | Yes | Array of node definitions that make up the workflow |
context | No | Key-value data accessible to all nodes via {{ context.key }} |
webhookUrl | No | URL to notify when the workflow completes |
workflowConfig | No | Workflow-level settings such as execution timeout |
The DAG structure prevents infinite loops (no cycles allowed) and lets Graph Compose run independent nodes in parallel automatically.
Nodes
Each node is a unit of work in the graph. Most nodes are HTTP requests to endpoints you control. Graph Compose calls your endpoints, you do not need to change your existing APIs.
There are six node types:
| Type | What it does | Learn more |
|---|---|---|
http | Makes an HTTP request to your endpoint | Node Types |
error_boundary | Catches failures in protected nodes and calls a cleanup endpoint | Error Boundaries |
source_iterator | Reads rows from a data source and spawns one child workflow per row | Node Types |
destination | Writes results back to a destination such as Google Sheets | Node Types |
confirmation | Pauses the workflow and waits for human approval before continuing | Node Types |
adk | Runs a multi-agent AI workflow with LLM agents, tools, and orchestration | AI Agents |
You do not need to change your existing APIs. Graph Compose calls your endpoints over HTTP. Your services stay where they are.
An HTTP node looks like this:
HTTP Node
{
"id": "get_order",
"type": "http",
"dependencies": [],
"http": {
"method": "GET",
"url": "https://api.example.com/orders/{{ context.orderId }}",
"headers": {
"Authorization": "Bearer {{ $secret('api_token') }}"
}
},
"activityConfig": {
"retryPolicy": {
"maximumAttempts": 3,
"initialInterval": "1s",
"backoffCoefficient": 2
},
"startToCloseTimeout": "30s"
}
}
Each node has an id (alphanumeric and underscores only), a type, and configuration specific to that type. The activityConfig is optional and controls retries and timeouts at the Temporal activity level.
Dependencies and Execution Order
Nodes declare which other nodes they depend on using the dependencies array. Graph Compose resolves the full DAG and determines which nodes can run in parallel.
In this example:
get_orderruns first (no dependencies)check_inventoryandget_shipping_ratesrun in parallel (both depend only onget_order)confirm_orderruns last (depends on bothcheck_inventoryandget_shipping_rates)
You do not need to think about parallelism explicitly. If two nodes have no dependency relationship, Graph Compose runs them concurrently.
Context and Results
Data flows through a workflow in three ways: context, results, and secrets.
Context
Context is data you provide when submitting the workflow. Every node can access it via {{ context.key }}.
Providing Context
{
"nodes": [...],
"context": {
"orderId": "order_123",
"userId": "user_456"
}
}
Inside any node, reference it as {{ context.orderId }} or {{ context.userId }}.
Results
When a node completes, its response is stored under results.<nodeId>. The result object has this shape:
Node Result Shape
{
"data": { ... },
"statusCode": 200,
"headers": { "content-type": "application/json" }
}
To access a field from the response body, use {{ results.nodeId.data.field }}. For example, if get_order returns { "items": [...], "total": 49.99 }, downstream nodes access it as:
{{ results.get_order.data.items }}for the items array{{ results.get_order.data.total }}for the total{{ results.get_order.statusCode }}for the HTTP status code
Secrets
Inject credentials without exposing them in the workflow JSON using {{ $secret('name') }}. Secrets are stored securely and resolved at runtime. See Secrets for setup.
Template expressions
All dynamic values use {{ }} delimiters with JSONata syntax. You can use JSONata operators, functions, and conditionals inside expressions:
{{ results.get_order.data.total * 1.1 }}
{{ $uppercase(results.get_order.data.customer_name) }}
{{ results.check_status.data.ready = true ? "proceed" : "wait" }}
See Template Syntax for the full expression reference.
A Complete Example
Here is a four-node order processing workflow. It fetches an order, checks inventory, reserves inventory, and processes payment, each step depending on the previous one.
import { GraphCompose } from '@graph-compose/client'
const graph = new GraphCompose({
token: process.env.GRAPH_COMPOSE_TOKEN
})
graph
.node("get_order")
.get("https://api.example.com/orders/{{ context.orderId }}")
.withRetries({
maximumAttempts: 3,
initialInterval: "1s"
})
.end()
.node("check_inventory")
.post("https://api.example.com/inventory/check")
.withBody({
items: "{{ results.get_order.data.items }}"
})
.withDependencies(["get_order"])
.end()
.node("reserve_inventory")
.post("https://api.example.com/inventory/reserve")
.withBody({
items: "{{ results.get_order.data.items }}"
})
.withDependencies(["check_inventory"])
.end()
.node("process_payment")
.post("https://api.example.com/payments")
.withBody({
orderId: "{{ results.get_order.data.id }}",
amount: "{{ results.get_order.data.total }}"
})
.withDependencies(["reserve_inventory"])
.withStartToCloseTimeout("30s")
.end()
When submitted, Graph Compose runs these nodes in sequence (each depends on the previous one). If get_order fails, Temporal retries it up to 3 times with exponential backoff. If process_payment exceeds 30 seconds, it times out. All state is persisted, so if a worker crashes mid-workflow, Temporal replays from the last checkpoint.
Building this manually means writing your own retry loops with backoff, tracking state for rollbacks, handling partial failures across parallel calls, managing timeouts with abort controllers, and ensuring cleanup runs even when errors cascade. That code grows quickly and tends to obscure the business logic it surrounds. Graph Compose moves all of that into configuration so your workflow definition stays declarative.
Execution Lifecycle
Once you have a workflow definition, the lifecycle is: validate, execute, query.
Validate
Check your workflow before submitting it. The SDK validates node IDs, dependencies, circular references, and JSONata expressions.
Validate
const validation = graph.validate()
if (!validation.isValid) {
console.error(validation.errors)
}
Execute
Submit the workflow. Graph Compose returns a workflow ID immediately and begins execution asynchronously.
Execute
const result = await graph.execute({
context: { orderId: "order_123" }
})
if (result.success && result.data) {
console.log("Workflow ID:", result.data.workflowId)
console.log("Status:", result.data.status) // "RUNNING"
}
Query status
Poll for status or use a webhook to be notified on completion.
Query Status
const status = await graph.getWorkflowStatus(workflowId)
if (status.success && status.data) {
// Possible: RUNNING, COMPLETED, FAILED, CANCELLED,
// TERMINATED, CONTINUED_AS_NEW, TIMED_OUT
console.log("Status:", status.data.status)
if (status.data.status === "COMPLETED") {
console.log("Results:", status.data.execution_state)
}
}
Alternatively, set a webhookUrl on the workflow to receive a POST request when execution completes.