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:
pnpm run devThe 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:
// 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
@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
@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
@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
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:
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: 15Using Port Types
ChainGraph supports various port types:
String Ports
@Input()
@String({ defaultValue: 'Hello' })
message: string = 'Hello'Boolean Ports
@Input()
@Boolean({ defaultValue: false })
enabled: boolean = falseArray Ports
@Input()
@PortArrayNumber({ defaultValue: [1, 2, 3] })
numbers: number[] = []Object Ports
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
@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:
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:
- Open the UI: Navigate to
http://localhost:3004 - Create a Flow: Click "New Flow"
- Add Nodes: Drag nodes from the sidebar onto the canvas
- Connect Ports: Click and drag from one port to another
- Set Values: Click a node to edit port values in the inspector
- Execute: Click the "Run" button to execute the flow
- View Results: See real-time updates as nodes execute
Debugging
Enable Breakpoints
You can add breakpoints to pause execution:
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:
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:
- Architecture Overview - Understand the system design
- API Reference - Explore the full API
- Node Decorators Guide - Learn advanced node creation
- Port System Guide - Deep dive into ports
Common Patterns
Conditional Execution
Use port visibility rules to show/hide ports based on conditions:
@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:
@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:
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
- Always initialize nodes - Call
node.initialize()after creation - Use descriptive names - Make titles and descriptions clear
- Validate inputs - Check input values before processing
- Handle errors - Return error information in
NodeExecutionResult - Keep nodes focused - One responsibility per node
- Document complex logic - Add comments to explain non-obvious code
- 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