CrystalFlow uses TypeScript decorators to define nodes, inputs, outputs, and properties.
Requires experimentalDecorators: true in your tsconfig.json.
@defineNode
Define a node type with metadata.
@defineNode(metadata: NodeMetadata)
Node metadata configuration:
type: string - Unique node type identifier
label: string - Display name
category: string - Category for grouping
description?: string - Optional description
Example:
@defineNode({
type: 'math.add',
label: 'Add Numbers',
category: 'Math',
description: 'Adds two numbers together'
})
class AddNode extends Node {
// ...
}
Define an input port on a node.
@Input(config: InputConfig)
Input configuration:
type: string - Data type ('string', 'number', 'boolean', 'any', etc.)
label: string - Display label
required?: boolean - Whether input is required (default: true)
defaultValue?: any - Default value if not connected
description?: string - Optional description
Example:
@Input({ type: 'number', label: 'First Number', required: true })
a: number = 0;
@Input({ type: 'string', label: 'Optional Text', required: false })
text?: string;
@Output
Define an output port on a node.
@Output(config: OutputConfig)
Output configuration:
type: string - Data type
label: string - Display label
description?: string - Optional description
Example:
@Output({ type: 'number', label: 'Result' })
result: number;
@Output({ type: 'string', label: 'Message' })
message: string;
@Property
Define a static configuration property.
@Property(config: PropertyConfig)
Property configuration:
type: 'string' | 'number' | 'boolean' | 'select' - Property type
label: string - Display label
defaultValue?: any - Default value
required?: boolean - Whether required
description?: string - Optional description
For numbers:
min?: number - Minimum value
max?: number - Maximum value
step?: number - Increment step
For select:
options: Array<{ value: string; label: string }> - Dropdown options
Example:
@Property({
type: 'string',
label: 'API URL',
defaultValue: 'https://api.example.com',
required: true
})
url: string = 'https://api.example.com';
@Property({
type: 'number',
label: 'Timeout (ms)',
defaultValue: 5000,
min: 1000,
max: 60000,
step: 1000
})
timeout: number = 5000;
@Property({
type: 'select',
label: 'Method',
defaultValue: 'GET',
options: [
{ value: 'GET', label: 'GET' },
{ value: 'POST', label: 'POST' }
]
})
method: string = 'GET';
@Property
Static configuration - Set in property panel, not connected to other nodes.
Use for: URLs, timeouts, mode selection, formatting options.
@Input
Dynamic data flow - Connected via handles to other node outputs.
Use for: Data processing, computed values, workflow results.
Example showing both:
@defineNode({
type: 'http.request',
label: 'HTTP Request',
category: 'HTTP'
})
class HttpRequestNode extends Node {
// Property: Static configuration
@Property({
type: 'string',
label: 'Base URL',
defaultValue: 'https://api.example.com'
})
baseUrl: string = 'https://api.example.com';
// Input: Dynamic data from another node
@Input({ type: 'string', label: 'Endpoint' })
endpoint: string = '';
@Output({ type: 'any', label: 'Response' })
response: any;
async execute() {
const url = `${this.baseUrl}${this.endpoint}`;
const res = await fetch(url);
this.response = await res.json();
}
}
All decorator metadata is stored using reflect-metadata:
import 'reflect-metadata';
// Get node metadata
const metadata = Reflect.getMetadata('node:definition', NodeClass);
// Get input metadata
const inputs = Reflect.getMetadata('node:inputs', NodeClass.prototype);
// Get output metadata
const outputs = Reflect.getMetadata('node:outputs', NodeClass.prototype);
// Get property metadata
const properties = Reflect.getMetadata('node:properties', NodeClass.prototype);
Inheritance
Decorators support inheritance:
class BaseNode extends Node {
@Property({ type: 'boolean', label: 'Enable Logging' })
enableLogging: boolean = false;
}
@defineNode({ type: 'custom.node', label: 'Custom' })
class CustomNode extends BaseNode {
// Inherits enableLogging property automatically
@Input({ type: 'string', label: 'Data' })
data: string = '';
}
TypeScript Configuration
Add to your tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"target": "ES2020",
"lib": ["ES2020"]
}
}
Complete Example
import { Node, defineNode, Input, Output, Property } from '@crystalflow/core';
@defineNode({
type: 'text.format',
label: 'Format Text',
category: 'Text',
description: 'Formats text with various options'
})
class FormatTextNode extends Node {
// Properties: Static configuration
@Property({
type: 'select',
label: 'Case',
defaultValue: 'none',
options: [
{ value: 'none', label: 'No Change' },
{ value: 'upper', label: 'UPPERCASE' },
{ value: 'lower', label: 'lowercase' }
]
})
caseStyle: string = 'none';
@Property({
type: 'boolean',
label: 'Trim Whitespace',
defaultValue: true
})
trim: boolean = true;
// Inputs: Dynamic data
@Input({ type: 'string', label: 'Text', required: true })
text: string = '';
@Input({ type: 'string', label: 'Prefix', required: false })
prefix?: string;
// Outputs: Results
@Output({ type: 'string', label: 'Formatted' })
formatted: string;
execute() {
let result = this.text;
// Apply prefix
if (this.prefix) {
result = this.prefix + result;
}
// Apply case
switch (this.caseStyle) {
case 'upper':
result = result.toUpperCase();
break;
case 'lower':
result = result.toLowerCase();
break;
}
// Apply trim
if (this.trim) {
result = result.trim();
}
this.formatted = result;
}
}