This guide explains how to create custom nodes for the DAAV platform backend.
DAAV allows developers to extend the platform by creating custom nodes. The platform supports different types of nodes:
- Input Nodes: Read data from sources
- Transform Nodes: Process and transform data
- Output Nodes: Write data to destinations
All nodes inherit from a base Node class, with specialized classes like InputNode, OutputNode, and TransformNode.
All nodes access their configuration through the inherited self.data property:
# Configuration is stored in self.data (inherited from base Node class)
config = self.data.get('config', {})
operation_type = config.get('operation_type', 'default')
# Example configuration structure
self.data = {
'config': {
'operation_type': 'filter',
'filter_column': 'status',
'filter_value': 'active'
},
'parquetSave': {'value': False}
}Create a new file in app/nodes/transforms directory, inheriting from TransformNode or child:
# filepath: app/nodes/transforms/my_custom_transform.py
from typing import Optional, Any
import pandas as pd
from pydantic import ConfigDict
from app.enums.status_node import StatusNode
from app.models.interface.node_data import NodeDataPandasDf, NodeDataParquet
from app.nodes.transforms.transform_node import TransformNode
from app.utils.utils import generate_pandas_schema
class MyCustomTransform(TransformNode):
"""
Custom transformation node.
Only implements the required methods inherited from TransformNode.
"""
def __init__(self, id: str, data: Any, revision: Optional[str] = None,
status: Optional[StatusNode] = None):
# Call parent constructor
super().__init__(id=id, data=data, revision=revision, status=status)
# Set default configuration if not provided
if not self.data.get('config'):
self.data['config'] = {
'operation_type': 'filter',
'threshold': 0.5,
'enabled': True
}
def process(self, sample=False) -> StatusNode:
"""
Main processing method - required by parent class.
This is the ONLY method you must implement.
"""
try:
# Get node configuration from self.data (inherited property)
config = self.data.get('config', {})
operation_type = config.get('operation_type', 'filter')
threshold = config.get('threshold', 0.5)
enabled = config.get('enabled', True)
if not enabled:
# Skip processing if disabled
return StatusNode.Valid
# Process inputs (inherited from parent: self.inputs)
for input_name, input_node in self.inputs.items():
node_data = input_node.get_node_data()
if isinstance(node_data, NodeDataPandasDf):
df = node_data.dataExample if sample else node_data.data
result_df = self._apply_transformation(df, config)
# Create output data
output_data = NodeDataPandasDf(
nodeSchema=generate_pandas_schema(result_df),
data=result_df,
dataExample=result_df.head(20),
name="Custom Transform Result"
)
# Set output (inherited method: self.outputs)
self.outputs.get('out').set_node_data(output_data, self)
return StatusNode.Valid
except Exception as e:
# Use inherited properties for error handling
import traceback
traceback.print_exc()
# On error you can fill the statusMessage.
# This information is postponed to the front interface for the user
self.statusMessage = str(e)
self.status = StatusNode.Error
# Also postponed for node with StatusNode.Error to the user Array of string or stacktrace
self.errorStackTrace = traceback.TracebackException.from_exception(e).format()
return StatusNode.Error
def _apply_transformation(self, df: pd.DataFrame, config: dict) -> pd.DataFrame:
"""Helper method - not inherited, your custom logic."""
result_df = df.copy()
# Use config from self.data
operation_type = config.get('operation_type', 'filter')
if operation_type == "filter":
filter_column = config.get('filter_column')
filter_value = config.get('filter_value')
if filter_column and filter_column in result_df.columns:
result_df = result_df[result_df[filter_column] == filter_value]
return result_df
def _validate_configuration(self) -> bool:
"""Validate that all required configuration parameters are present."""
config = self.data.get('config', {})
required_params = ['operation_type', 'threshold']
for param in required_params:
if param not in config:
self.statusMessage = f"Missing required parameter: {param}"
return False
# Validate parameter values
if config.get('threshold') < 0 or config.get('threshold') > 1:
self.statusMessage = "Threshold must be between 0 and 1"
return False
return True
model_config = ConfigDict(arbitrary_types_allowed=True)Create a new file in app/nodes/inputs/ directory, inheriting strictly from InputNode:
# filepath: app/nodes/inputs/my_custom_input.py
from typing import Optional, Any
from pydantic import ConfigDict
import pandas as pd
from app.enums.status_node import StatusNode
from app.models.interface.node_data import NodeDataPandasDf
from app.nodes.inputs.input_node import InputNode
from app.utils.utils import generate_pandas_schema
class MyCustomInput(InputNode):
"""
Custom input node.
Only implements the required methods inherited from InputNode.
"""
def __init__(self, id: str, data: Any, revision: Optional[str] = None,
status: Optional[StatusNode] = None):
# Call parent constructor
super().__init__(id=id, data=data, revision=revision, status=status)
def process(self, sample: bool = False) -> StatusNode:
"""
Main processing method - required by parent class.
This is the ONLY method you must implement.
"""
try:
# Get configuration from self.data (inherited property)
config = self.data.get('config', {})
data_source = self.data.get('selectDataSource', {}).get('value')
# Your custom data reading logic here
df = self._read_custom_data(sample, config)
# Create node data
node_data = NodeDataPandasDf(
nodeSchema=generate_pandas_schema(df),
data=df,
dataExample=df.head(20),
name="Custom Input Data"
)
# Set data to outputs (inherited property: self.outputs)
for key, output in self.outputs.items():
output.set_node_data(node_data, self)
return StatusNode.Valid
except Exception as e:
# Use inherited properties for error handling
import traceback
traceback.print_exc()
self.errorStackTrace = traceback.format_exc()
self.statusMessage = str(e)
return StatusNode.Error
def _read_custom_data(self, sample: bool, config: dict) -> pd.DataFrame:
"""Helper method - not inherited, your custom data reading logic."""
# Use configuration from self.data
source_type = config.get('source_type', 'default')
# Example: read from your custom source
if sample:
return pd.DataFrame({'sample_col': [1, 2, 3]})
else:
return pd.DataFrame({'sample_col': range(100)})
model_config = ConfigDict(arbitrary_types_allowed=True)Create a new file in app/nodes/outputs/ directory, inheriting strictly from OutputNode:
# filepath: app/nodes/outputs/my_custom_output.py
from typing import Optional, Any
from pydantic import ConfigDict
from app.enums.status_node import StatusNode
from app.models.interface.node_data import NodeDataPandasDf, NodeDataParquet
from app.nodes.outputs.output_node import OutputNode
class MyCustomOutput(OutputNode):
"""
Custom output node.
Only implements the required methods inherited from OutputNode.
"""
def __init__(self, id: str, data: Any, revision: Optional[str] = None,
status: Optional[StatusNode] = None):
# Call parent constructor
super().__init__(id=id, data=data, revision=revision, status=status)
def process(self, sample=False) -> StatusNode:
"""
Main processing method - required by parent class.
This is the ONLY method you must implement.
"""
try:
# Get configuration from self.data (inherited property)
config = self.data.get('config', {})
destination = self.data.get('selectDataSource', {}).get('value')
# Process all inputs (inherited property: self.inputs)
for input_name, input_node in self.inputs.items():
node_data = input_node.get_node_data()
if isinstance(node_data, NodeDataPandasDf):
df = node_data.dataExample if sample else node_data.data
self._write_custom_data(df, sample, config)
elif isinstance(node_data, NodeDataParquet):
# Handle parquet data
import pyarrow.parquet as pq
df = pd.read_parquet(node_data.data)
if sample:
df = df.head(20)
self._write_custom_data(df, sample, config)
return StatusNode.Valid
except Exception as e:
# Use inherited properties for error handling
import traceback
traceback.print_exc()
self.errorStackTrace = traceback.format_exc()
self.statusMessage = str(e)
return StatusNode.Error
def _write_custom_data(self, df, sample: bool, config: dict):
"""Helper method - not inherited, your custom data writing logic."""
# Use configuration from self.data
output_format = config.get('output_format', 'csv')
# Example: write to your custom destination
print(f"Writing {len(df)} rows to custom destination (sample={sample}, format={output_format})")
model_config = ConfigDict(arbitrary_types_allowed=True)To add custom configuration parameters to your nodes, extend the data() method and handle them in the constructor:
class MyCustomTransform(TransformNode):
def __init__(self, id: str, data: Any, revision: Optional[str] = None,
status: Optional[StatusNode] = None):
super().__init__(id=id, data=data, revision=revision, status=status)
# Set default configuration if not provided
if not self.data.get('config'):
self.data['config'] = {
'operation_type': 'filter',
'threshold': 0.5,
'enabled': True
}
def process(self, sample=False) -> StatusNode:
# Access configuration parameters
config = self.data.get('config', {})
operation_type = config.get('operation_type', 'filter')
threshold = config.get('threshold', 0.5)
enabled = config.get('enabled', True)
if not enabled:
# Skip processing if disabled
return StatusNode.Valid
# Use configuration in your processing logic
# ... rest of implementationImplement configuration validation to ensure required parameters are present:
def _validate_configuration(self) -> bool:
"""Validate that all required configuration parameters are present."""
config = self.data.get('config', {})
required_params = ['operation_type', 'threshold']
for param in required_params:
if param not in config:
self.statusMessage = f"Missing required parameter: {param}"
return False
# Validate parameter values
if config.get('threshold') < 0 or config.get('threshold') > 1:
self.statusMessage = "Threshold must be between 0 and 1"
return False
return TrueNode (app/nodes/node.py - base class)
├── InputNode (app/nodes/inputs/input_node.py)
│ ├── DataFileBlock (data_file_block.py) - File data input
│ ├── DataHuggingBlock (data_hugging_block.py) - Hugging Face data input
│ ├── DataLrsBlock (data_lrs_block.py) - LRS data input
│ ├── DataMongoBlock (data_mongo_block.py) - MongoDB data input
│ ├── DataMysqlBlock (data_mysql_block.py) - MySQL data input
│ ├── ServiceChainInput (service-chain-input.py) - Service Chain data input
│ └── ExampleInput (example_input.py) - Demo/example data generator
│
├── OutputNode (app/nodes/outputs/output_node.py)
│ ├── ApiOutput (api_output.py) - API endpoint output
│ ├── FileOutput (file_output.py) - File output
│ ├── MongoOutput (mongo_output.py) - MongoDB output
│ ├── MysqlOutput (mysql_output.py) - MySQL output
│ ├── PdcOutput (pdc_output.py) - PDC Chain output
│ ├── ServiceChainOutput (service_chain_output.py) - Service Chain output
│ └── ExampleOutput (example_ouput.py) - Demo/example output
│
└── TransformNode (app/nodes/transforms/transform_node.py)
├── MergeTransform (merge_transform.py) - Merge columns from multiple sources
├── FlattenTransform (flatten_transform.py) - Flatten nested JSON structures
├── FilterTransform (filter_transform.py) - Filter data based on conditions
└── ExampleTransform (example_transform.py) - Demo/example transform
- DataFileBlock: Read data from file datasets
- ExampleInput: Demo/example data generator
- DataMongoBlock: Read from MongoDB collections
- DataMysqlBlock: Read from MySQL databases
- DataApiBlock: Read from API endpoints
- DataElasticBlock: Read from Elasticsearch
- DataHuggingBlock: Import from Hugging Face datasets
- DataLrsBlock: Read from LRS (Learning Record Store)
- ServiceChainInput: Fetch data from Service Chain services
Your platform supports these output destinations:
- FileOutput: Write data to files
- MongoOutput: Write to MongoDB collections
- MysqlOutput: Write to MySQL databases
- ApiOutput: Send data to API endpoints
- PdcOutput: Send data to PDC Chain service
- ServiceChainOutput: Send data to Service Chain services
- ExampleOutput: Demo/example output
Your platform includes these data transformation capabilities:
- MergeTransform: Combine columns from multiple data sources
- FlattenTransform: Flatten nested JSON/object structures into tabular format
- FilterTransform: Apply conditional filtering to datasets using SQL-like expressions
- ExampleTransform: Demo/example transformation
Your platform uses a factory pattern (node_factory.py) to create and manage node instances dynamically using automatic class scanning.
Your custom node should follow the same patterns as MergeTransform and FlattenTransform:
- Inherit from
TransformNode - Implement
process()method with sample support - Support both pandas and parquet modes
- Use
generate_pandas_schema()for output schemas - Handle errors with proper status and stack traces
- Support the same input/output node data types
Follow the configuration pattern from existing transforms:
# Example configuration structure (similar to MergeTransform)
data = {
'config': {
# Your custom configuration here
},
'parquetSave': {'value': False} # Support parquet mode
}Follow the established pattern for handling multiple inputs and setting outputs:
# Input processing (from existing transforms)
for input_name, input_node in self.inputs.items():
node_data = input_node.get_node_data()
# Process based on type...
# Output setting (standard pattern)
self.outputs.get('out').set_node_data(output_data, self)- Use the same imports and type hints as existing transforms
- Follow the same error handling patterns
- Use the same status management approach
- Implement the
process()method with support for both pandas DataFrame and parquet data - Handle sample mode consistently using the
sampleparameter - Support the same data types as existing transforms (NodeDataPandasDf, NodeDataParquet)
- Follow the existing test patterns in
tests/nodes/ - Test all the same scenarios as existing transform tests
- Include fixtures following the established naming conventions
- Document your transform's purpose and configuration
- Provide examples similar to existing transforms
- Include usage patterns
This guide focuses on the backend extension capabilities based on your existing codebase architecture. The frontend integration would depend on the Angular application structure, which would need to be analyzed separately.
The DAAV frontend uses Rete.js with Angular for visual workflow editing. Nodes are implemented as TypeScript classes that extend base classes and are registered using the @daavBlock decorator.
This decorator takes an optional tag string parameter that can be used for filtering and ordering.
To allow automatic registration we need to import the code of the new implementation
by adding a link inside src/app/nodes/index.ts
src/app/nodes/
├── node-block.ts # Base class for all nodes
├── input/
│ └── input-data-block.ts # Base class for input nodes
├── output/
│ └── output-data-block.ts # Base class for output nodes
├── transform/
│ └── transform-block.ts # Base class for transform nodes
├── inputs/ # Concrete input node implementations
├── outputs/ # Concrete output node implementations
├── transforms/ # Concrete transform node implementations
└── index.ts # Export all nodes
NodeBlock (base class)
├── InputDataBlock (for data sources)
│ ├── DataFileBlock
│ ├── DataApiBlock
│ ├── DataElasticBlock
│ ├── DataMongoBlock
│ ├── DataMysqlBlock
│ ├── DataLrsBlock
│ ├── ServiceChainInput
│ └── ExampleInput
├── OutputDataBlock (for data destinations)
│ ├── FileOutput
│ ├── MongoOutput
│ ├── MysqlOutput
│ ├── ApiOutput
│ ├── PdcOutput
│ ├── ServiceChainOutput
│ └── ExampleOutput
└── TransformBlock (for data transformations)
├── MergeTransform
├── FlattenTransform
├── FilterTransform
└── ExampleTransform
Create a new file in src/app/nodes/transforms/:
// filepath: src/app/nodes/transforms/my-custom-transform.ts
import { ClassicPreset } from "rete";
import { AreaPlugin } from "rete-area-plugin";
import { FlatObjectSocket } from "src/app/core/sockets/sockets";
import { Schemes, AreaExtra } from "src/app/core/workflow-editor";
import { StatusNode } from "src/app/enums/status-node";
import { daavBlock } from "../node-block";
import { Node } from "src/app/models/interfaces/node";
import { TransformBlock } from "../transform/transform-block";
@daavBlock('transform')
export class MyCustomTransform extends TransformBlock {
override width = 350;
override height = 200;
constructor(
label: string,
area: AreaPlugin<Schemes, AreaExtra>,
node?: Node
) {
super(label, area, node);
if (!node) {
// Set initial status
this.status = StatusNode.Incomplete;
// Add inputs and outputs
this.addInput(
"input",
new ClassicPreset.Input(new FlatObjectSocket(), "Input")
);
this.addOutput(
"output",
new ClassicPreset.Output(new FlatObjectSocket(), "Output")
);
}
// Update node status based on configuration
this.validateConfiguration();
}
override data() {
// Merge your custom data with parent data
const customData = {
config: {
operation_type: this.getOperationType(),
filter_column: this.getFilterColumn(),
filter_value: this.getFilterValue()
}
};
return { ...super.data(), ...customData };
}
private validateConfiguration() {
// Implement your validation logic
const config = this.data()?.config || {};
if (config.operation_type && config.filter_column) {
this.updateStatus(StatusNode.Complete);
} else {
this.updateStatus(StatusNode.Incomplete);
}
}
private getOperationType(): string {
// Get from your controls or default value
return 'filter';
}
private getFilterColumn(): string {
// Get from your controls or default value
return '';
}
private getFilterValue(): string {
// Get from your controls or default value
return '';
}
override execute() {
// This method is called when the play button is clicked
console.log('Executing custom transform with config:', this.data().config);
}
}Create a new file in src/app/nodes/inputs/:
// filepath: src/app/nodes/inputs/my-custom-input.ts
import { ClassicPreset } from "rete";
import { AreaPlugin } from "rete-area-plugin";
import { FlatObjectSocket } from "src/app/core/sockets/sockets";
import { Schemes, AreaExtra } from "src/app/core/workflow-editor";
import { StatusNode } from "src/app/enums/status-node";
import { daavBlock } from "../node-block";
import { Node } from "src/app/models/interfaces/node";
import { InputDataBlock } from "../input/input-data-block";
@daavBlock('input')
export class MyCustomInput extends InputDataBlock {
override width = 350;
override height = 200;
constructor(
label: string,
area: AreaPlugin<Schemes, AreaExtra>,
node?: Node
) {
super(label, area, node);
if (!node) {
// Set initial status
this.status = StatusNode.Incomplete;
// Add outputs (inputs only have outputs)
this.addOutput(
"output",
new ClassicPreset.Output(new FlatObjectSocket(), "Data")
);
}
// Add custom configuration if needed
this.setupCustomConfiguration(node);
}
override getRevision(): string {
// Return a revision string based on your configuration
const data = this.data();
return JSON.stringify({
dataSource: data.selectDataSource?.value,
config: data.config
});
}
override data() {
// Merge your custom data with parent data
const customData = {
config: {
source_type: this.getSourceType(),
connection_string: this.getConnectionString()
}
};
return { ...super.data(), ...customData };
}
private setupCustomConfiguration(node?: Node) {
// Add custom controls/widgets here if needed
// Example: this.addCustomControl();
this.validateConfiguration();
}
private validateConfiguration() {
// Implement your validation logic
const hasDataSource = this.data().selectDataSource?.value;
if (hasDataSource) {
this.updateStatus(StatusNode.Complete, "Data source configured");
} else {
this.updateStatus(StatusNode.Incomplete, "Select a data source");
}
}
private getSourceType(): string {
// Return your source type
return 'custom_api';
}
private getConnectionString(): string {
// Return connection configuration
return '';
}
override execute() {
// This method is called when the play button is clicked
console.log('Executing custom input with config:', this.data());
}
}Create a new file in src/app/nodes/outputs/:
// filepath: src/app/nodes/outputs/my-custom-output.ts
import { ClassicPreset } from "rete";
import { AreaPlugin } from "rete-area-plugin";
import { FlatObjectSocket } from "src/app/core/sockets/sockets";
import { Schemes, AreaExtra } from "src/app/core/workflow-editor";
import { StatusNode } from "src/app/enums/status-node";
import { daavBlock } from "../node-block";
import { Node } from "src/app/models/interfaces/node";
import { OutputDataBlock } from "../output/output-data-block";
@daavBlock('output')
export class MyCustomOutput extends OutputDataBlock {
override width = 350;
override height = 200;
constructor(
label: string,
area: AreaPlugin<Schemes, AreaExtra>,
node?: Node
) {
super(label, area, node);
if (!node) {
// Set initial status
this.status = StatusNode.Incomplete;
// Add inputs (outputs only have inputs)
this.addInput(
"input",
new ClassicPreset.Input(new FlatObjectSocket(), "Data")
);
}
// Add custom configuration if needed
this.setupCustomConfiguration(node);
}
override getRevision(): string {
// Return a revision string based on your configuration
const data = this.data();
return JSON.stringify({
dataSource: data.selectDataSource?.value,
config: data.config
});
}
override data() {
// Merge your custom data with parent data
const customData = {
config: {
output_format: this.getOutputFormat(),
destination_path: this.getDestinationPath()
}
};
return { ...super.data(), ...customData };
}
private setupCustomConfiguration(node?: Node) {
// Add custom controls/widgets here if needed
this.validateConfiguration();
}
private validateConfiguration() {
// Implement your validation logic
const hasDataSource = this.data().selectDataSource?.value;
if (hasDataSource) {
this.updateStatus(StatusNode.Complete, "Output destination configured");
} else {
this.updateStatus(StatusNode.Incomplete, "Select output destination");
}
}
private getOutputFormat(): string {
// Return your output format
return 'json';
}
private getDestinationPath(): string {
// Return destination configuration
return '';
}
override execute() {
// This method is called when the play button is clicked
console.log('Executing custom output with config:', this.data());
}
}Backend (app/nodes/transforms/my_custom_transform.py):
from app.nodes.transforms.transform_node import TransformNode
from app.enums.status_node import StatusNode
class MyCustomTransform(TransformNode):
def process(self, sample=False) -> StatusNode:
# Your implementation here
return StatusNode.ValidFrontend (src/app/nodes/transforms/my-custom-transform.ts):
import { TransformBlock } from "../transform/transform-block";
import { daavBlock } from "../node-block";
@daavBlock('transform')
export class MyCustomTransform extends TransformBlock {
override getRevision(): string { return "1.0"; }
override execute(): void { /* implementation */ }
}Export (src/app/nodes/index.ts):
export * from "./transforms/my-custom-transform";Backend:
from typing import Optional, Any
import pandas as pd
from pydantic import ConfigDict
from app.enums.status_node import StatusNode
from app.models.interface.node_data import NodeDataPandasDf, NodeDataParquet
from app.utils.utils import generate_pandas_schemaFrontend:
import { ClassicPreset } from "rete";
import { AreaPlugin } from "rete-area-plugin";
import { StatusNode } from "src/app/enums/status-node";
import { FlatObjectSocket } from "src/app/core/sockets/sockets";
import { Schemes, AreaExtra } from "src/app/core/workflow-editor";
import { Node } from "src/app/models/interfaces/node";- Backend Debugging: Use
print()statements or Python debugger in theprocess()method - Frontend Debugging: Use
console.log()in browser developer tools - Configuration Issues: Check the
data()method output in frontend andself.datacontent in backend - Status Problems: Verify status is set correctly using
StatusNodeenum values - Connection Issues: Ensure socket types match between connected nodes
Create test files in the tests/nodes/ directory following the existing pattern:
# filepath: tests/nodes/transforms/test_my_custom_transform.py
import pytest
import pandas as pd
from app.nodes.transforms.my_custom_transform import MyCustomTransform
from app.enums.status_node import StatusNode
from app.models.interface.node_data import NodeDataPandasDf
from app.utils.utils import generate_pandas_schema
class TestMyCustomTransform:
def test_process_valid_data(self):
"""Test processing with valid input data."""
# Create test data
test_data = pd.DataFrame({
'name': ['Alice', 'Bob', 'Charlie'],
'age': [25, 30, 35],
'status': ['active', 'inactive', 'active']
})
# Create node with test configuration
node_data = {
'config': {
'operation_type': 'filter',
'filter_column': 'status',
'filter_value': 'active'
}
}
transform = MyCustomTransform(
id="test_transform",
data=node_data
)
# Set up input data
input_data = NodeDataPandasDf(
nodeSchema=generate_pandas_schema(test_data),
data=test_data,
dataExample=test_data.head(20),
name="Test Data"
)
# Mock input connection
transform.inputs['input'].set_node_data(input_data, transform)
# Execute the transform
result = transform.process(sample=False)
# Verify results
assert result == StatusNode.Valid
output_data = transform.outputs['output'].get_node_data()
assert len(output_data.data) == 2 # Only 'active' records
def test_process_invalid_config(self):
"""Test processing with invalid configuration."""
transform = MyCustomTransform(
id="test_transform",
data={'config': {}} # Missing required config
)
result = transform.process(sample=False)
assert result == StatusNode.Error
assert "Missing required parameter" in transform.statusMessageTest your nodes in the context of a complete workflow:
def test_transform_in_workflow():
"""Test the transform as part of a complete workflow."""
from app.core.workflow import Workflow
from app.models.interface.workflow_interface import IProject
# Create a simple workflow with your custom transform
project_data = {
"schema": {
"nodes": [
{
"id": "input_1",
"type": "ExampleInput",
"data": {}
},
{
"id": "transform_1",
"type": "MyCustomTransform",
"data": {
"config": {
"operation_type": "filter",
"filter_column": "age",
"filter_value": 30
}
}
}
],
"connections": [
{
"id": "conn_1",
"sourceNode": "input_1",
"sourcePort": "output",
"targetNode": "transform_1",
"targetPort": "input"
}
]
}
}
# Execute the workflow
workflow = Workflow()
workflow.import_project(IProject(**project_data))
result = workflow.execute()
# Verify the workflow executed successfully
assert result == StatusNode.Valid- Always extend the appropriate base class (
InputDataBlock,OutputDataBlock,TransformBlock) - Use the
@daavBlockdecorator with the correct type - Implement required abstract methods (
getRevision(),execute())
- Store configuration in the
data()method return object - Support node restoration from existing configuration
- Validate configuration and update status accordingly
- Set initial status in constructor (
StatusNode.Incompletefor new nodes) - Update status when configuration changes
- Provide meaningful status messages
- Use appropriate socket types from
src/app/core/sockets/sockets - Follow input/output patterns from existing nodes
- Ensure socket compatibility between connected nodes
When you create a custom node, you need to ensure both frontend and backend implementations use the same class names:
Backend: MyCustomTransform in app/nodes/transforms/my_custom_transform.py
Frontend: MyCustomTransform in src/app/nodes/transforms/my-custom-transform.ts
The frontend node sends its configuration to the backend through the data() method:
// Frontend - data() method returns configuration
override data() {
return {
config: {
operation_type: this.operationType,
filter_column: this.filterColumn
},
parquetSave: this.parquetCheckbox
};
}# Backend - receives configuration through self.data
def process(self, sample=False) -> StatusNode:
config = self.data.get('config', {})
operation_type = config.get('operation_type')
filter_column = config.get('filter_column')
# ... process using configurationThe backend node status is automatically synchronized to the frontend:
# Backend - set status and messages
self.status = StatusNode.Error
self.statusMessage = "Configuration error"
self.errorStackTrace = ["Error details..."]// Frontend - status is automatically updated
this.updateStatus(StatusNode.Error, "Configuration error", ["Error details..."]);This guide provides the information needed to create custom nodes for the DAAV platform, covering both backend and frontend implementations, configuration management, and best practices for development and testing.