Skip to main content
CrystalFlow uses TypeScript decorators to define nodes declaratively. Decorators provide metadata about nodes, inputs, outputs, and properties without cluttering your business logic.

What are Decorators?

Decorators are special annotations that add metadata to classes and properties:
@defineNode({ type: 'math.add', label: 'Add' })
class AddNode extends Node {
  @Input({ type: 'number', label: 'A' })
  a: number = 0;
  
  @Output({ type: 'number', label: 'Result' })
  result: number;
}
Decorators are a TypeScript feature that requires experimentalDecorators: true in your tsconfig.json

@defineNode

The @defineNode decorator registers a node class with CrystalFlow:
@defineNode({
  type: 'category.nodename',
  label: 'Display Name',
  category: 'Category',
  description: 'Optional description'
})
class MyNode extends Node {
  // ...
}

Parameters

type
string
required
Unique identifier for the node type (e.g., 'math.add', 'http.request')Convention: Use dot notation like category.operation
label
string
required
Human-readable name shown in the UI
category
string
required
Category for organizing nodes in the palette (e.g., 'Math', 'Data', 'Network')
description
string
Optional description explaining what the node does

Example

@defineNode({
  type: 'string.uppercase',
  label: 'To Uppercase',
  category: 'String',
  description: 'Converts a string to uppercase'
})
class UppercaseNode extends Node {
  execute() {
    this.output = this.input.toUpperCase();
  }
}

@Input

The @Input decorator defines an input port on a node:
@Input({
  type: 'string',
  label: 'Message',
  defaultValue: 'Hello',
  required: false,
  description: 'Input message'
})
message: string = 'Hello';

Parameters

type
string
required
Data type of the input: 'string', 'number', 'boolean', 'any', or custom types
label
string
required
Display name for the input port in the UI
defaultValue
any
Default value if no connection is made
required
boolean
default:"false"
Whether this input must be connected or have a value
description
string
Optional description of the input’s purpose

Example

@defineNode({
  type: 'math.power',
  label: 'Power',
  category: 'Math'
})
class PowerNode extends Node {
  @Input({ 
    type: 'number', 
    label: 'Base',
    defaultValue: 2,
    description: 'The base number'
  })
  base: number = 2;

  @Input({ 
    type: 'number', 
    label: 'Exponent',
    defaultValue: 2,
    description: 'The power to raise to'
  })
  exponent: number = 2;

  @Output({ type: 'number', label: 'Result' })
  result: number;

  execute() {
    this.result = Math.pow(this.base, this.exponent);
  }
}

@Output

The @Output decorator defines an output port on a node:
@Output({
  type: 'string',
  label: 'Result',
  description: 'The processed result'
})
result: string;

Parameters

type
string
required
Data type of the output: 'string', 'number', 'boolean', 'any', or custom types
label
string
required
Display name for the output port in the UI
description
string
Optional description of what the output represents

Example

@defineNode({
  type: 'http.request',
  label: 'HTTP Request',
  category: 'Network'
})
class HttpRequestNode extends Node {
  @Output({ 
    type: 'any', 
    label: 'Response',
    description: 'HTTP response data'
  })
  response: any;

  @Output({ 
    type: 'number', 
    label: 'Status Code',
    description: 'HTTP status code'
  })
  statusCode: number;

  @Output({ 
    type: 'any', 
    label: 'Headers',
    description: 'Response headers'
  })
  headers: any;

  async execute() {
    const res = await fetch(this.url);
    this.response = await res.json();
    this.statusCode = res.status;
    this.headers = Object.fromEntries(res.headers.entries());
  }
}

@Property

The @Property decorator defines a static configuration value (not connected to other nodes):
@Property({
  type: 'select',
  label: 'Operation',
  defaultValue: 'add',
  options: [
    { value: 'add', label: 'Add' },
    { value: 'subtract', label: 'Subtract' }
  ],
  description: 'The operation to perform'
})
operation: string = 'add';

Parameters

type
'string' | 'number' | 'boolean' | 'select'
required
The property type
label
string
required
Display label in the properties panel
defaultValue
any
Default value for the property
required
boolean
default:"false"
Whether the property is required
description
string
Description shown in the UI
options
Array<{value: any, label: string}>
Options for select type properties
min
number
Minimum value for number properties
max
number
Maximum value for number properties
step
number
Step increment for number properties

Example

@defineNode({
  type: 'data.filter',
  label: 'Filter Array',
  category: 'Data'
})
class FilterNode extends Node {
  @Property({
    type: 'select',
    label: 'Condition Type',
    defaultValue: 'equals',
    options: [
      { value: 'equals', label: 'Equals' },
      { value: 'contains', label: 'Contains' },
      { value: 'greater', label: 'Greater Than' }
    ]
  })
  conditionType: string = 'equals';

  @Property({
    type: 'string',
    label: 'Field Name',
    defaultValue: 'id',
    description: 'The field to filter by'
  })
  fieldName: string = 'id';

  @Property({
    type: 'number',
    label: 'Max Results',
    defaultValue: 100,
    min: 1,
    max: 1000,
    step: 10
  })
  maxResults: number = 100;

  @Input({ type: 'any[]', label: 'Array' })
  array: any[] = [];

  @Output({ type: 'any[]', label: 'Filtered' })
  filtered: any[];

  execute() {
    // Use properties in logic
    this.filtered = this.array
      .filter(item => this.applyCondition(item))
      .slice(0, this.maxResults);
  }
}

Property vs Input

Key Difference:
  • @Property = Static configuration in properties panel (not connected)
  • @Input = Dynamic data flow via connections
  • @Output = Computed results during execution
class ExampleNode extends Node {
  // Property: Set once in UI, doesn't change
  @Property({ type: 'string', label: 'API Endpoint' })
  endpoint: string = 'https://api.example.com';

  // Input: Receives data from other nodes
  @Input({ type: 'string', label: 'Query' })
  query: string = '';

  // Output: Produces data for other nodes
  @Output({ type: 'any', label: 'Data' })
  data: any;

  async execute() {
    // Use property and input to produce output
    const response = await fetch(`${this.endpoint}?q=${this.query}`);
    this.data = await response.json();
  }
}

Decorator Inheritance

Properties and decorators are inherited from base classes:
// Base class with common properties
class BaseApiNode extends Node {
  @Property({
    type: 'boolean',
    label: 'Enable Logging',
    defaultValue: false
  })
  enableLogging: boolean = false;

  @Property({
    type: 'number',
    label: 'Timeout (ms)',
    defaultValue: 30000
  })
  timeout: number = 30000;
}

// Child class inherits properties
@defineNode({
  type: 'api.users',
  label: 'Fetch Users',
  category: 'API'
})
class FetchUsersNode extends BaseApiNode {
  // Inherits enableLogging and timeout

  @Output({ type: 'any[]', label: 'Users' })
  users: any[];

  async execute() {
    if (this.enableLogging) {
      console.log('Fetching users...');
    }
    // Use inherited timeout property
    // ...
  }
}

TypeScript Configuration

To use decorators, configure your tsconfig.json:
tsconfig.json
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "target": "ES2020",
    "module": "ESNext"
  }
}
Don’t forget reflect-metadata!Import reflect-metadata at the top of your entry file:
import 'reflect-metadata';

Best Practices

Labels should be clear and concise - they’re shown to users in the UI.
Always add descriptions to help users understand what inputs, outputs, and properties do.
Provide default values that work out of the box.
Name node types with category prefixes: math.add, string.uppercase, http.request

Complete Example

Here’s a complete node using all decorators:
import 'reflect-metadata';
import { Node, defineNode, Property, Input, Output } from '@crystalflow/core';

@defineNode({
  type: 'text.replace',
  label: 'Replace Text',
  category: 'Text',
  description: 'Replace occurrences of text in a string'
})
export class ReplaceTextNode extends Node {
  // Property: Configuration
  @Property({
    type: 'boolean',
    label: 'Case Sensitive',
    defaultValue: true,
    description: 'Whether matching should be case sensitive'
  })
  caseSensitive: boolean = true;

  @Property({
    type: 'boolean',
    label: 'Replace All',
    defaultValue: true,
    description: 'Replace all occurrences or just the first'
  })
  replaceAll: boolean = true;

  // Inputs: Dynamic data
  @Input({
    type: 'string',
    label: 'Text',
    required: true,
    description: 'The text to search in'
  })
  text: string = '';

  @Input({
    type: 'string',
    label: 'Find',
    required: true,
    description: 'The text to find'
  })
  find: string = '';

  @Input({
    type: 'string',
    label: 'Replace',
    defaultValue: '',
    description: 'The replacement text'
  })
  replace: string = '';

  // Outputs: Results
  @Output({
    type: 'string',
    label: 'Result',
    description: 'The text with replacements made'
  })
  result: string;

  @Output({
    type: 'number',
    label: 'Count',
    description: 'Number of replacements made'
  })
  count: number;

  // Business logic
  execute() {
    let searchText = this.text;
    let findText = this.find;

    if (!this.caseSensitive) {
      searchText = searchText.toLowerCase();
      findText = findText.toLowerCase();
    }

    if (this.replaceAll) {
      const regex = new RegExp(
        findText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'),
        this.caseSensitive ? 'g' : 'gi'
      );
      this.result = this.text.replace(regex, this.replace);
      this.count = (this.text.match(regex) || []).length;
    } else {
      const index = searchText.indexOf(findText);
      if (index !== -1) {
        this.result = 
          this.text.slice(0, index) +
          this.replace +
          this.text.slice(index + this.find.length);
        this.count = 1;
      } else {
        this.result = this.text;
        this.count = 0;
      }
    }
  }
}

Next Steps