| name | port-system |
| description | Deep dive into ChainGraph port system - 9 port types, plugins, factory, transfer rules, and frontend rendering. Use when creating custom ports, working on port validation, implementing port UI components, or debugging port compatibility. Triggers: port, PortString, PortNumber, PortArray, PortObject, PortStream, port type, port plugin, port factory, transfer rules, port validation, port compatibility. |
ChainGraph Port System
This skill provides a deep dive into the ChainGraph port system - the typed input/output connectors that enable data flow between nodes.
Port System Overview
Ports are strongly-typed connectors on nodes. The system consists of:
- 9 Port Types - string, number, boolean, array, object, stream, enum, secret, any
- Port Instances - Runtime port objects with values
- Port Plugins - Validation and serialization per type
- Port Factory - Creates port instances from configs
- Transfer Rules - Type compatibility for connections
- Frontend Stores - ports-v2 Effector stores for UI
The 9 Port Types
Scalar Types
| Type | Decorator | Config | Use Cases |
|---|
string | @PortString() | defaultValue, minLength, maxLength, pattern, multiline | Text, prompts, IDs, URLs |
number | @PortNumber() | defaultValue, min, max, step, integer | Counts, scores, coordinates |
boolean | @PortBoolean() | defaultValue | Flags, toggles, conditions |
Complex Types
| Type | Decorator | Config | Use Cases |
|---|
array | @PortArray() | itemConfig, minLength, maxLength, isMutable | Lists, batches, collections |
object | @PortObject() | schema, isSchemaMutable | Structured data, records, configs |
stream | @PortStream() | itemConfig | Real-time data, LLM tokens, multi-channel |
Special Types
| Type | Decorator | Config | Use Cases |
|---|
enum | @PortEnum() | options: EnumOption[] | Dropdowns, fixed choices |
secret | @PortSecret() | secretType | API keys, passwords, tokens |
any | @Any() | - | Dynamic data (avoid if possible) |
Port Configuration Details
StringPortConfig
File: packages/chaingraph-types/src/port/base/types.ts
interface StringPortConfig {
type: 'string'
defaultValue?: string
minLength?: number
maxLength?: number
pattern?: string
multiline?: boolean
ui?: PortUIConfig
}
@Input()
@PortString({
defaultValue: '',
maxLength: 10000,
multiline: true,
})
prompt: string = ''
NumberPortConfig
interface NumberPortConfig {
type: 'number'
defaultValue?: number
min?: number
max?: number
step?: number
integer?: boolean
ui?: PortUIConfig
}
@Input()
@PortNumber({
defaultValue: 0.7,
min: 0,
max: 2,
step: 0.1,
})
temperature: number = 0.7
ArrayPortConfig
interface ArrayPortConfig<Item extends IPortConfig> {
type: 'array'
itemConfig: Item
defaultValue?: Array<ExtractValue<Item>>
minLength?: number
maxLength?: number
isMutable?: boolean
isSchemaMutable?: boolean
ui?: PortUIConfig
}
@Input()
@PortArray({
itemConfig: { type: 'string', defaultValue: '' },
isMutable: true,
minLength: 1,
})
items: string[] = ['']
@Input()
@PortArray({
itemConfig: {
type: 'object',
schema: {
properties: {
name: { type: 'string' },
value: { type: 'number' },
}
}
}
})
records: Array<{ name: string, value: number }> = []
ObjectPortConfig
interface ObjectPortConfig<Schema extends IObjectSchema> {
type: 'object'
schema: Schema
defaultValue?: ExtractObjectValue<Schema>
isSchemaMutable?: boolean
ui?: PortUIConfig
}
interface IObjectSchema {
properties: Record<string, IPortConfig>
required?: string[]
}
@Input()
@PortObject({
schema: {
properties: {
model: { type: 'string', defaultValue: 'gpt-4' },
maxTokens: { type: 'number', defaultValue: 1000, min: 1 },
stream: { type: 'boolean', defaultValue: false },
},
required: ['model'],
},
})
config: { model: string, maxTokens: number, stream: boolean }
StreamPortConfig
File: packages/chaingraph-types/src/port/base/types.ts:178-184
interface StreamPortConfig<Item extends IPortConfig> {
type: 'stream'
itemConfig: Item
isSchemaMutable?: boolean
defaultValue?: StreamPortValue<Item>
ui?: PortUIConfig
}
type StreamPortValue<T extends IPortConfig> = MultiChannel<ExtractValue<T>>
IMPORTANT: Stream values are MultiChannel<T>, NOT AsyncIterable<T>.
MultiChannel: packages/chaingraph-types/src/utils/multi-channel.ts
class MultiChannel<T> {
send(value: T): void
sendBatch(values: T[]): void
close(): void
setError(error: Error): void
getError(): Error | null
[Symbol.asyncIterator](): AsyncIterableIterator<T>
isChannelClosed(): boolean
getBuffer(): T[]
getSubscriberCount(): number
}
Example: Token stream from LLM
@Output()
@PortStream({
title: 'Response Stream',
description: 'Streamed response from Claude',
itemConfig: { type: 'string' },
})
responseStream: MultiChannel<string> = new MultiChannel<string>()
async execute(context: ExecutionContext) {
context.resolvePort(this.id, 'responseStream')
for await (const event of llmStream) {
this.responseStream.send(event.delta.text || '')
}
this.responseStream.close()
}
async execute(context: ExecutionContext) {
for await (const token of this.responseStream) {
console.log('Token:', token)
}
}
Example: Multi-channel with structured data
@Output()
@PortStream({
itemConfig: {
type: 'object',
schema: {
properties: {
type: { type: 'string' },
data: { type: 'any' },
}
}
}
})
events: MultiChannel<{ type: string, data: any }> = new MultiChannel()
async execute(context: ExecutionContext) {
this.events.send({ type: 'start', data: null })
this.events.send({ type: 'progress', data: { percent: 50 } })
this.events.send({ type: 'complete', data: { result: 'done' } })
this.events.close()
}
EnumPortConfig
File: packages/chaingraph-types/src/port/base/types.ts:198-203
interface EnumPortConfig {
type: 'enum'
options: IPortConfig[]
defaultValue?: EnumPortValue
ui?: PortUIConfig
}
IMPORTANT: Options are IPortConfig[], not simple value/label pairs. Each option is a complete port configuration.
Decorators: packages/chaingraph-types/src/decorator/enum.decorator.ts
@Input()
@PortEnum({
options: [
{ id: 'gpt-4', type: 'string', defaultValue: 'gpt-4', title: 'GPT-4' },
{ id: 'gpt-3.5', type: 'string', defaultValue: 'gpt-3.5-turbo', title: 'GPT-3.5 Turbo' },
{ id: 'claude', type: 'string', defaultValue: 'claude-3', title: 'Claude 3' },
],
defaultValue: 'gpt-4',
})
model: string = 'gpt-4'
@Input()
@StringEnum(['Red', 'Green', 'Blue'], { defaultValue: 'Red' })
color: string = 'Red'
@Input()
@NumberEnum([1, 2, 3, 4, 5], { defaultValue: 1 })
rating: number = 1
SecretPortConfig
interface SecretPortConfig<SecretType extends string> {
type: 'secret'
secretType: SecretType
defaultValue: undefined
ui?: PortUIConfig
}
@Input()
@PortSecret({ secretType: 'openai-api-key' })
apiKey: Secret<'openai-api-key'>
Stream Port Best Practices
Always initialize MultiChannel:
@Output()
@PortStream({ itemConfig: { type: 'string' } })
tokens: MultiChannel<string> = new MultiChannel<string>()
@Output()
@PortStream({ itemConfig: { type: 'string' } })
tokens: MultiChannel<string>
Always close streams when done:
async execute(context: ExecutionContext) {
try {
for (const item of items) {
this.outputStream.send(item)
}
} finally {
this.outputStream.close()
}
}
Handle stream errors:
async execute(context: ExecutionContext) {
try {
} catch (error) {
this.outputStream.setError(error)
throw error
}
}
async execute(context: ExecutionContext) {
for await (const item of this.inputStream) {
}
const error = this.inputStream.getError()
if (error) {
throw new Error(`Stream error: ${error.message}`)
}
}
Resolve ports for parallel consumption:
async execute(context: ExecutionContext) {
context.resolvePort(this.id, 'responseStream')
for await (const event of llmApi) {
this.responseStream.send(event.text)
}
this.responseStream.close()
}
Port Plugins
Each port type has a dedicated plugin for validation and serialization.
Location: packages/chaingraph-types/src/port/plugins/
Plugin Interface
interface PortPlugin<C extends IPortConfig, V> {
validateConfig(config: C): ValidationError[]
validateValue(value: V, config: C): ValidationError[]
serializeConfig(config: C): JSONValue
serializeValue(value: V): JSONValue
deserializeConfig(data: JSONValue): C
deserializeValue(data: JSONValue): V
getDefaultValue(config: C): V | undefined
}
Plugin Registry
import { PortPluginRegistry } from '@badaitech/chaingraph-types'
const stringPlugin = PortPluginRegistry.getPlugin('string')
const errors = stringPlugin.validateValue('test', config)
Port Factory
Creates port instances from configuration.
File: packages/chaingraph-types/src/port/factory/PortFactory.ts
import { PortFactory } from '@badaitech/chaingraph-types'
const stringPort = PortFactory.create({
type: 'string',
defaultValue: 'hello',
maxLength: 100,
})
const ports = PortFactory.createFromConfigs([
{ type: 'string', id: 'input' },
{ type: 'number', id: 'count', defaultValue: 0 },
])
const arrayPort = PortFactory.create<ArrayPortC
onfig<StringPortConfig>>({
type: 'array',
itemConfig: { type: 'string' },
})
Transfer Rules (Port Compatibility)
Determines which port types can connect to each other.
Location: packages/chaingraph-types/src/port/transfer-rules/
Default Transfer Rules
| Source | Target | Rule |
|---|
string | string | Direct transfer |
number | number | Direct transfer |
number | string | Auto-convert to string |
boolean | boolean | Direct transfer |
array<T> | array<T> | Item-wise transfer |
object | object | Schema compatibility check |
any | * | Always compatible |
* | any | Always compatible |
Using Transfer Engine
import { getDefaultTransferEngine, canConnect } from '@badaitech/chaingraph-types'
const compatible = canConnect(sourcePort, targetPort)
const engine = getDefaultTransferEngine()
const result = engine.canTransfer(sourceConfig, targetConfig)
Custom Transfer Rules
import { RuleBuilder, Predicates, Strategies } from '@badaitech/chaingraph-types'
const customRule = RuleBuilder.create()
.when(Predicates.sourceIs('number'))
.when(Predicates.targetIs('string'))
.then(Strategies.convert((value) => String(value)))
.build()
Frontend Port Stores (ports-v2)
The frontend uses Effector stores for port state management.
Location: apps/chaingraph-frontend/src/store/ports-v2/
Store Organization
store/ports-v2/
├── domain.ts # portsV2Domain
├── stores.ts # Main stores
├── events.ts # Events for updates
├── effects.ts # Server sync effects
├── selectors.ts # Derived stores
├── buffer.ts # Event batching
├── echo-detection.ts # Echo prevention
├── pending-mutations.ts # Optimistic tracking
├── collapsed-handles.ts # UI collapse state
└── descendants.ts # Array/Object child tracking
Key Stores
export const $portValues = portsV2Domain.createStore<Map<string, unknown>>(new Map())
export const $portConfigs = portsV2Domain.createStore<Map<string, IPortConfig>>(new Map())
export const $portUI = portsV2Domain.createStore<Map<string, PortUIState>>(new Map())
export const $collapsedHandles = portsV2Domain.createStore<Set<string>>(new Set())
Using Port Stores in Components
import { useUnit } from 'effector-react'
import { $portValues, updatePortValue } from '@/store/ports-v2'
function PortInput({ portKey }: { portKey: string }) {
const [portValues, update] = useUnit([$portValues, updatePortValue])
const value = portValues.get(portKey)
return (
<input
value={value ?? ''}
onChange={(e) => update({ portKey, value: e.target.value })}
/>
)
}
Port UI Components
Location: apps/chaingraph-frontend/src/components/flow/nodes/ChaingraphNode/ports/
Port Component Hierarchy
ports/
├── PortRenderer.tsx # Routes to correct port component
├── ScalarPort/
│ ├── StringPortInput.tsx # Text input/textarea
│ ├── NumberPortInput.tsx # Number input/slider
│ └── BooleanPortInput.tsx # Checkbox/toggle
├── ArrayPort/
│ └── ArrayPort.tsx # List with add/remove
├── ObjectPort/
│ └── ObjectPort.tsx # Nested property editors
├── EnumPort/
│ └── EnumPortSelect.tsx # Dropdown select
└── StreamPort/
└── StreamPortDisplay.tsx # Real-time display
Port Rendering Pattern
function PortRenderer({ nodeId, portKey, config }: PortRendererProps) {
switch (config.type) {
case 'string':
return <StringPortInput nodeId={nodeId} portKey={portKey} config={config} />
case 'number':
return <NumberPortInput nodeId={nodeId} portKey={portKey} config={config} />
case 'array':
return <ArrayPort nodeId={nodeId} portKey={portKey} config={config} />
case 'object':
return <ObjectPort nodeId={nodeId} portKey={portKey} config={config} />
}
}
Key Files Reference
| File | Purpose |
|---|
types/src/port/base/IPort.ts | Port interface (212 lines) |
types/src/port/base/types.ts | All config types |
types/src/port/instances/ | 9 port implementations |
types/src/port/plugins/ | Validation plugins |
types/src/port/factory/PortFactory.ts | Port creation |
types/src/port/transfer-rules/ | Compatibility rules |
frontend/src/store/ports-v2/ | Frontend Effector stores |
frontend/src/components/flow/nodes/ChaingraphNode/ports/ | UI components |
Common Patterns
Creating a Port with Validation
@Input()
@PortString({
defaultValue: '',
minLength: 1,
maxLength: 100,
pattern: '^[a-zA-Z0-9_]+$',
})
@Description('Unique identifier (alphanumeric)')
identifier: string = ''
Conditional Port Visibility
@Input()
@PortEnum({
options: [
{ value: 'simple', label: 'Simple' },
{ value: 'advanced', label: 'Advanced' },
],
defaultValue: 'simple',
})
mode: 'simple' | 'advanced' = 'simple'
@PortVisibility({ showIf: (node) => node.getPort('mode')?.getValue() === 'advanced' })
@Input()
@PortObject({ schema: advancedSchema })
advancedConfig: AdvancedConfig
Mutable Array Port
@Input()
@PortArray({
itemConfig: { type: 'string', defaultValue: '' },
isMutable: true,
minLength: 1,
maxLength: 10,
})
tags: string[] = ['default']
Related Skills
types-architecture - Complete types package overview
frontend-architecture - Frontend store organization
effector-patterns - Store patterns for ports-v2
chaingraph-concepts - What ports ARE conceptually