Core Concepts

Graph Compose makes it easy to build and manage complex API workflows. Here's what you need to know to get started.

Nodes ๐Ÿ”ต

Basic units of work that:

  • โ€ข Make HTTP requests to APIs
  • โ€ข Transform data with JSONata
  • โ€ข Handle retries and timeouts

Dependencies โžก๏ธ

Control execution flow:

  • โ€ข Define execution order
  • โ€ข Enable parallel processing
  • โ€ข Manage data flow

Context ๐ŸŒ

Global workflow state:

  • โ€ข Available to all expressions
  • โ€ข Context is stored under context
  • โ€ข Previously executed nodes results are stored under [nodeId].result

JSONata ๐Ÿ”„

Transform data between nodes:

  • โ€ข Powerful expression language for JSON
  • โ€ข Access data from any node using dot notation
  • โ€ข Transform, filter, and combine data seamlessly

Workflow Basics

A workflow in Graph Compose is represented as a directed acyclic graph (DAG). Think of a graph like a flowchart where each step (a node) performs an action, and arrows dictate the sequence and dependencies. In Graph Compose, each node typically represents an HTTP request.

This DAG structure ensures a clear, predictable execution order, prevents infinite loops (as it's 'acyclic', meaning no cycles), and allows independent nodes to run in parallel for efficiency.

Nodes are connected, allowing the output from one node (like an API response) to be transformed and used as input for subsequent nodes.

You have several options for defining these workflows:

  • Code SDKs: Leverage our type-safe SDKs for maximum control, flexibility, and integration with your existing codebase.
  • Visual Builder: Use the graphical interface (shown below) to quickly design, visualize, and manage workflow connections.
  • Direct API (JSON): Define workflows by sending JSON definitions to our REST API, allowing for dynamic generation or augmentation from your custom applications.
Graph Compose Workflow Builder showing a sample e-commerce order processing workflow

Building a Real-World Workflow: E-commerce Order Processing

Let's illustrate the power of Graph Compose with a common scenario: processing an e-commerce order. This involves coordinating multiple API calls (fetching order details, checking/reserving inventory, processing payment) with reliability. While you could attempt to chain these calls manually, Graph Compose provides an enterprise-ready solution built on Temporal.io for durable, zero-deploy orchestration, freeing you from complex infrastructure management.

SOLUTION One Simple API, Zero Infrastructure

Graph Compose handles all the complexity for you:

  • ๐ŸŽฏ Automatic Retries: Built-in retry policies for transient failures
  • ๐Ÿ›ก๏ธ Error Boundaries: Sophisticated error handling and recovery
  • ๐Ÿ“Š Real-time Monitoring: Watch your workflow execute in real-time
  • ๐Ÿ”„ State Management: Durable execution with automatic state persistence

The Graph Compose Way ๐Ÿš€

Here's how you define the order processing workflow using Graph Compose:

const workflow = new GraphCompose()
  // Define error boundary to handle rollback if inventory reservation fails
  .errorBoundary("inventory_rollback", ["reserve_inventory"])
    .post("https://your-api.com/api/inventory/release")
    .withBody({
      items: "{{results.get_order.items}}"
    })
  .end()

  // Define workflow nodes in order of execution
  .node("get_order")
    .get("https://your-api.com/api/orders/{{context.orderId}}")
    // Configure activity-level settings like retries
    .withRetries({
      maximumAttempts: 3,
      initialInterval: "1s"  // Uses exponential backoff
    })
  .end()

  .node("check_inventory")
    .post("https://your-api.com/api/inventory/check")
    .withBody({ items: "{{results.get_order.items}}" })
    .withDependencies(["get_order"])  // Requires order data
  .end()

  .node("reserve_inventory")
    .post("https://your-api.com/api/inventory/reserve")
    .withBody({ items: "{{results.get_order.items}}" })
    .withDependencies(["check_inventory"])  // Only reserve after checking
  .end()

  .node("process_payment")
    .post("https://your-api.com/api/payments")
    .withBody({
      orderId: "{{results.get_order.id}}",
      amount: "{{results.get_order.total}}"
    })
    .withDependencies(["reserve_inventory"])
    // Configure activity-level settings like timeouts
    .withStartToCloseTimeout("30s")
  .end();

// Execute the workflow with automatic retries, error handling, and monitoring
const result = await workflow.execute({ orderId: "123" });

To understand the benefits Graph Compose provides, let's look at how this same workflow might be built using a traditional, manual approach.

Compare: The Traditional (Manual) Approach That Breaks Down ๐Ÿ˜ซ

Here's what the same e-commerce order processing workflow looks like when implemented manually with TypeScript, perhaps in a single serverless function. You can immediately see the challenges in managing timeouts, retries, state, and error handling.

The Hard Way ๐Ÿ˜ซ

async function processOrder(orderId: string) {
  // Need to track state for rollbacks ๐Ÿ˜ซ
  const reservedInventory = []

  try {
    // Get order details - with retries because production is flaky
    let order
    let retries = 3
    while (retries > 0) {
      try {
        const response = await fetch("https://your-api.com/api/orders/" + orderId)
        if (!response.ok) throw new Error(`HTTP ${response.status}`)
        order = await response.json()
        break
      } catch (e) {
        retries--
        if (retries === 0)
          throw new Error("Failed to fetch order after 3 attempts: " + e.message)
        await new Promise((r) => setTimeout(r, 1000 * (3 - retries))) // Backoff
      }
    }

    // Check inventory for all items - parallel but need to handle partial failures
    const inventoryChecks = await Promise.all(
      order.items.map(async (item, idx) => {
        try {
          const response = await fetch("https://your-api.com/api/inventory/" + item.productId)
          if (!response.ok) throw new Error(`HTTP ${response.status}`)
          const inv = await response.json()

          if (inv.quantity < item.quantity) {
            throw new Error("Insufficient inventory for " + item.productId)
          }
          return inv
        } catch (e) {
          // Should we fail everything or just this item? ๐Ÿค”
          console.error(`Item ${idx + 1}/${order.items.length} check failed:`, e)
          throw new Error(`Inventory check failed for item ${item.productId}: ${e.message}`)
        }
      })
    )

    // Reserve inventory - need to track for rollback if later steps fail
    for (const [idx, item] of order.items.entries()) {
      try {
        const response = await fetch("https://your-api.com/api/inventory/" + item.productId + "/reserve", {
          method: "POST",
          body: JSON.stringify({ quantity: item.quantity })
        })

        if (!response.ok) {
          // Uh oh, need to roll back previous reservations! ๐Ÿ˜ฑ
          for (const prevItem of reservedInventory) {
            try {
              await fetch("https://your-api.com/api/inventory/" + prevItem.productId + "/release", {
                method: "POST",
                body: JSON.stringify({ quantity: prevItem.quantity })
              })
            } catch (releaseError) {
              console.error("Failed to release inventory:", releaseError)
              // Now we're in an inconsistent state! What do we do? ๐Ÿ˜ฑ
            }
          }
          throw new Error(`HTTP ${response.status}`)
        }

        reservedInventory.push(item)
      } catch (e) {
        throw new Error(`Failed to reserve inventory for item ${item.productId}: ${e.message}`)
      }
    }

    // Process payment - with timeout because payment APIs are slow
    const controller = new AbortController()
    const timeout = setTimeout(() => controller.abort(), 10000)

    try {
      const response = await fetch("https://your-api.com/api/payments", {
        method: "POST",
        body: JSON.stringify({
          orderId,
          amount: order.total
        }),
        signal: controller.signal
      })

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`)
      }
    } catch (e) {
      // Payment failed - need to release ALL inventory
      console.error("Payment failed, rolling back inventory reservations...")
      
      try {
        await Promise.all(reservedInventory.map(item =>
          fetch("https://your-api.com/api/inventory/" + item.productId + "/release", {
            method: "POST",
            body: JSON.stringify({ quantity: item.quantity })
          })
        ))
      } catch (releaseError) {
        console.error("Failed to release inventory after payment failure:", releaseError)
        // More inconsistent state! ๐Ÿ”ฅ
      }
      
      throw new Error(`Payment failed: ${e.message}`)
    } finally {
      clearTimeout(timeout)
    }

    return { success: true, orderId }
  } catch (error) {
    // Which operation failed? What's our current state?
    // How do we recover? Where do we retry?
    // Good luck debugging this in production! ๐Ÿ˜…
    console.error("Something went wrong:", error)
    throw error
  }
}
Scroll to see more

Why This Breaks Down in Production ๐Ÿ”ฅ

๐Ÿ˜ซ

Error Handling is Complex

  • Partial Failures: What happens when only some items in Promise.all fail?
  • Cascading Failures: If inventory release fails after payment failure, you're left in an inconsistent state
  • Retry Logic: Each operation needs its own retry strategy and backoff logic
  • Error Recovery: How do you resume from failures? Restart or continue?

๐Ÿ”

Debugging is a Nightmare

  • Flow Visibility: No way to see the workflow state in real-time
  • Error Context: When something fails, which step failed and why?
  • Logging Gaps: Console logs disappear in production
  • State Tracking: What's the current state during issues?

๐Ÿ› ๏ธ

Maintenance Becomes Impossible

  • Code Complexity: Error handling overshadows business logic
  • State Management: Need to track state for rollbacks and retries
  • Monitoring: How do you track success rates and patterns?
  • Testing: Testing all error paths becomes exponentially complex

๐Ÿ—๏ธ

Infrastructure Concerns

  • Timeouts: This service must stay alive for the duration of the workflow, waiting for responses from all processes and services
  • Durability: What happens if the server crashes mid-workflow?
  • Resource Management: Need to manage connections and cleanup

Want more examples?

Our hosted functions allow you to access a wide range of APIs and services, including Stripe, SendGrid, and more.

Next Steps

Ready to start building? Choose your path: