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"
  }
}
FieldRequiredDescription
nodesYesArray of node definitions that make up the workflow
contextNoKey-value data accessible to all nodes via {{ context.key }}
webhookUrlNoURL to notify when the workflow completes
workflowConfigNoWorkflow-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:

TypeWhat it doesLearn more
httpMakes an HTTP request to your endpointNode Types
error_boundaryCatches failures in protected nodes and calls a cleanup endpointError Boundaries
source_iteratorReads rows from a data source and spawns one child workflow per rowNode Types
destinationWrites results back to a destination such as Google SheetsNode Types
confirmationPauses the workflow and waits for human approval before continuingNode Types
adkRuns a multi-agent AI workflow with LLM agents, tools, and orchestrationAI Agents

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.

graph LR A[get_order] --> B[check_inventory] A --> C[get_shipping_rates] B --> D[confirm_order] C --> D

In this example:

  1. get_order runs first (no dependencies)
  2. check_inventory and get_shipping_rates run in parallel (both depend only on get_order)
  3. confirm_order runs last (depends on both check_inventory and get_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.

Next Steps