Skip to content

Quick Start

This guide will help you create your first ChainGraph flow in minutes.

Prerequisites

Make sure you have installed ChainGraph and have it running:

bash
pnpm run dev

The frontend should be accessible at http://localhost:3004 and the backend at http://localhost:3001.

Creating Your First Node

Let's create a simple node that adds two numbers. Create a new file in your workspace:

typescript
// my-nodes/addition.node.ts
import {
  BaseNode,
  Node,
  Input,
  Output,
  Number,
  ExecutionContext,
  NodeExecutionResult
} from '@badaitech/chaingraph-types'

@Node({
  type: 'AdditionNode',
  title: 'Addition',
  description: 'Adds two numbers together',
  category: 'math'
})
export class AdditionNode extends BaseNode {
  @Input()
  @Number({ defaultValue: 0 })
  a: number = 0

  @Input()
  @Number({ defaultValue: 0 })
  b: number = 0

  @Output()
  @Number()
  result: number = 0

  async execute(context: ExecutionContext): Promise<NodeExecutionResult> {
    this.result = this.a + this.b
    return {}
  }
}

Understanding the Node Structure

Let's break down the key parts:

1. Node Decorator

typescript
@Node({
  type: 'AdditionNode',
  title: 'Addition',
  description: 'Adds two numbers together',
  category: 'math'
})

The @Node decorator defines metadata about your node:

  • title: Display name in the UI
  • description: What the node does
  • category: Groups nodes in the UI

2. Input Ports

typescript
@Input()
@Number({ defaultValue: 0 })
a: number = 0
  • @Input() marks this as an input port
  • @Number() specifies the port type with configuration
  • Default value is used when no connection is made

3. Output Ports

typescript
@Output()
@Number({ defaultValue: 0 })
result: number = 0
  • @Output() marks this as an output port
  • The value is set during execute() and can be read by connected nodes

4. Execute Method

typescript
async execute(context: ExecutionContext): Promise<NodeExecutionResult> {
  this.result = this.a + this.b
  return {}
}

This method contains your node's logic. It's called during flow execution.

Creating a Flow Programmatically

Here's how to create and execute a flow in code:

typescript
import { Flow, ExecutionContext, ExecutionEngine } from '@badaitech/chaingraph-types'
import { AdditionNode } from './my-nodes/addition.node'

// Create a flow
const flow = new Flow({ name: 'My First Flow' })

// Create nodes
const node1 = new AdditionNode('add1')
node1.initialize()

// Set input values
node1.a = 5
node1.b = 10

// Add node to flow
await flow.addNode(node1)

// Execute the flow
const abortController = new AbortController()
const context = new ExecutionContext(flow.id, abortController)
const engine = new ExecutionEngine(flow, context)

// Subscribe to events
engine.onAll((event) => {
  console.log('Event:', event.type, event.data)
})

// Run the flow
await engine.execute()

// Check result
console.log('Result:', node1.result) // Output: 15

Using Port Types

ChainGraph supports various port types:

String Ports

typescript
@Input()
@String({ defaultValue: 'Hello' })
message: string = 'Hello'

Boolean Ports

typescript
@Input()
@Boolean({ defaultValue: false })
enabled: boolean = false

Array Ports

typescript
@Input()
@PortArrayNumber({ defaultValue: [1, 2, 3] })
numbers: number[] = []

Object Ports

typescript
import { ObjectSchema, PortObject } from '@badaitech/chaingraph-types'

@ObjectSchema({
  type: 'UserProfile'
})
class UserProfile {
  @String()
  name: string = ''

  @Number()
  age: number = 0
}

// In your node:
@Input()
@PortObject({ schema: UserProfile, defaultValue: new UserProfile() })
profile: UserProfile = new UserProfile()

Enum Ports

typescript
@Input()
@StringEnum(['Red', 'Green', 'Blue'], { defaultValue: 'Red' })
color: string = 'Red'

Creating a Multi-Node Flow

Let's create a more complex flow with multiple nodes:

typescript
import { Flow, ExecutionContext, ExecutionEngine } from '@badaitech/chaingraph-types'
import { AdditionNode } from './my-nodes/addition.node'

const flow = new Flow({ name: 'Multi-Node Flow' })

// Create multiple nodes
const add1 = new AdditionNode('add1')
const add2 = new AdditionNode('add2')
const add3 = new AdditionNode('add3')

// Initialize nodes
add1.initialize()
add2.initialize()
add3.initialize()

// Set values
add1.a = 5
add1.b = 10

add2.a = 3
add2.b = 7

// Add nodes to flow
await flow.addNode(add1)
await flow.addNode(add2)
await flow.addNode(add3)

// Connect nodes: add3 takes results from add1 and add2
// In production, you'd connect ports through the Flow API
// This is simplified for demonstration

// Execute
const context = new ExecutionContext(flow.id, new AbortController())
const engine = new ExecutionEngine(flow, context)
await engine.execute()

Using the Visual Editor

The ChainGraph frontend provides a visual interface:

  1. Open the UI: Navigate to http://localhost:3004
  2. Create a Flow: Click "New Flow"
  3. Add Nodes: Drag nodes from the sidebar onto the canvas
  4. Connect Ports: Click and drag from one port to another
  5. Set Values: Click a node to edit port values in the inspector
  6. Execute: Click the "Run" button to execute the flow
  7. View Results: See real-time updates as nodes execute

Debugging

Enable Breakpoints

You can add breakpoints to pause execution:

typescript
import { NodeStatus } from '@badaitech/chaingraph-types'

// In your node's execute method:
async execute(context: ExecutionContext): Promise<NodeExecutionResult> {
  // Add debug logging
  console.log('Executing with inputs:', this.a, this.b)

  this.result = this.a + this.b

  console.log('Result:', this.result)
  return {}
}

Monitor Events

Subscribe to execution events for debugging:

typescript
engine.on('nodeExecutionStarted', (event) => {
  console.log('Node started:', event.nodeId)
})

engine.on('nodeExecutionCompleted', (event) => {
  console.log('Node completed:', event.nodeId)
})

engine.on('nodeExecutionFailed', (event) => {
  console.error('Node failed:', event.nodeId, event.error)
})

Next Steps

Now that you've created your first flow, explore:

Common Patterns

Conditional Execution

Use port visibility rules to show/hide ports based on conditions:

typescript
@Boolean({ defaultValue: false })
advanced: boolean = false

@PortVisibility({
  showIf: (node) => (node as MyNode).advanced
})
@String({ defaultValue: '' })
advancedOption: string = ''

Stream Processing

Use stream ports for handling multiple values:

typescript
@Input()
@PortStream({ itemConfig: { type: 'string' } })
dataStream: MultiChannel<string> = new MultiChannel()

async execute(context: ExecutionContext): Promise<NodeExecutionResult> {
  for await (const item of this.dataStream) {
    console.log('Processing:', item)
  }
  return {}
}

Error Handling

Handle errors gracefully in your nodes:

typescript
async execute(context: ExecutionContext): Promise<NodeExecutionResult> {
  try {
    this.result = this.a + this.b
    return {}
  } catch (error) {
    return {
      error: error instanceof Error ? error.message : 'Unknown error'
    }
  }
}

Tips & Best Practices

  1. Always initialize nodes - Call node.initialize() after creation
  2. Use descriptive names - Make titles and descriptions clear
  3. Validate inputs - Check input values before processing
  4. Handle errors - Return error information in NodeExecutionResult
  5. Keep nodes focused - One responsibility per node
  6. Document complex logic - Add comments to explain non-obvious code
  7. Test nodes individually - Write unit tests for node logic

Need Help?

  • Check the API Reference for detailed documentation
  • Review existing nodes in packages/chaingraph-nodes/src/nodes/
  • Open an issue on GitHub

Licensed under BUSL-1.1