SDK Documentation

Build custom nodes, connectors, and plugins with the DeepChain SDK

DeepChain SDK Guide

Build your own custom nodes and connectors to extend DeepChain with custom logic. This guide walks you through everything, with complete working examples.

Table of Contents


Overview

The DeepChain SDK is your toolkit for extending DeepChain. You can build:

  • Custom Nodes — New workflow building blocks (process an API response, transform data, etc.)
  • Connectors — Integrations with external services (Salesforce, SAP, your API, etc.)
  • Plugins — Full extensions you can sell on the marketplace

How It Works

Your Custom Code (Dart)
        ↓
DeepChain SDK (interfaces & base classes)
        ↓
DeepChain Core (runs your code in workflows)
        ↓
Your workflows use it like any built-in node

All in Dart, with type safety and hot reload for fast development.


Installation

Step 1: Add the SDK to your project

Create or open your Dart/Flutter package's pubspec.yaml:

# pubspec.yaml
name: my_custom_nodes
description: Custom nodes for DeepChain
version: 1.0.0

dependencies:
  deepchain_sdk:
    git:
      url: https://github.com/deepchain/deepchain.git
      path: sdk

Step 2: Get dependencies

cd your_package
dart pub get

Now you're ready to build!


Build Your First Custom Node

Let's create a practical node that calculates order discounts. You'll see the complete process: define → execute → register → test.

Step 1: Create the node definition

Create lib/nodes/discount_calculator_node.dart:

import 'package:deepchain_sdk/deepchain_sdk.dart';

/// Calculates discount based on order amount.
/// Input: order amount
/// Output: discount percentage and final price
class DiscountCalculatorNode extends WorkflowNode {
  DiscountCalculatorNode({
    required super.id,
    super.position,
    super.data,
  }) : super(
    type: 'discount_calculator',
    name: 'Calculate Discount',
    description: 'Applies tiered discounts based on order amount',
    category: NodeCategory.data,

    // What inputs does this node accept?
    inputPorts: [
      InputPort(
        name: 'amount',
        type: PortType.number,
        required: true,
        description: 'Order amount in dollars',
      ),
    ],

    // What outputs does it produce?
    outputPorts: [
      OutputPort(
        name: 'discountPercent',
        type: PortType.number,
        description: 'Discount as percentage (0-50)',
      ),
      OutputPort(
        name: 'finalPrice',
        type: PortType.number,
        description: 'Price after discount',
      ),
    ],
  );

  @override
  DiscountCalculatorNode copyWith({
    String? id,
    NodePosition? position,
    Map<String, dynamic>? data,
  }) {
    return DiscountCalculatorNode(
      id: id ?? this.id,
      position: position ?? this.position,
      data: data ?? this.data,
    );
  }
}

Step 2: Create the executor (the logic)

Create lib/executors/discount_calculator_executor.dart:

import 'package:deepchain_sdk/deepchain_sdk.dart';

/// Executes the discount calculation logic
class DiscountCalculatorExecutor extends NodeExecutor<DiscountCalculatorNode> {
  @override
  Future<NodeExecutionResult> execute(
    DiscountCalculatorNode node,
    ExecutionContext context,
  ) async {
    try {
      // Get the input value
      final amount = context.getInput<num>('amount');
      if (amount == null) {
        return NodeExecutionResult.failure(
          error: 'Amount is required',
        );
      }

      // Calculate discount percentage based on amount
      final discountPercent = _calculateDiscount(amount.toDouble());

      // Calculate final price
      final finalPrice = amount * (1 - (discountPercent / 100));

      // Return results
      return NodeExecutionResult.success(
        outputs: {
          'discountPercent': discountPercent,
          'finalPrice': finalPrice,
        },
      );
    } catch (e) {
      return NodeExecutionResult.failure(
        error: 'Calculation failed: $e',
      );
    }
  }

  /// Business logic: tiered discounts
  double _calculateDiscount(double amount) {
    if (amount >= 1000) return 20; // $1000+ = 20% off
    if (amount >= 500) return 15;  // $500+ = 15% off
    if (amount >= 100) return 10;  // $100+ = 10% off
    if (amount >= 50) return 5;    // $50+ = 5% off
    return 0; // No discount
  }
}

Step 3: Register your node

Create lib/my_nodes.dart:

import 'package:deepchain_sdk/deepchain_sdk.dart';
import 'executors/discount_calculator_executor.dart';
import 'nodes/discount_calculator_node.dart';

/// Call this once when your app starts
void registerMyNodes() {
  // Register the node type
  NodeRegistry.register(
    'discount_calculator',
    DiscountCalculatorNode.new,
  );

  // Register the executor
  ExecutorRegistry.register(
    'discount_calculator',
    DiscountCalculatorExecutor(),
  );
}

Then in your app's main() or initialization:

void main() {
  registerMyNodes(); // Register before creating UI
  runApp(MyApp());
}

Step 4: Test your node

Create test/discount_calculator_test.dart:

import 'package:test/test.dart';
import 'package:my_custom_nodes/my_nodes.dart';

void main() {
  test('Calculates 10% discount for $100 order', () async {
    registerMyNodes();

    final executor = DiscountCalculatorExecutor();
    final node = DiscountCalculatorNode(id: 'test_1');

    // Create mock execution context
    final context = MockExecutionContext({
      'amount': 100,
    });

    final result = await executor.execute(node, context);

    expect(result.isSuccess, true);
    expect(result.outputs['discountPercent'], 10);
    expect(result.outputs['finalPrice'], 90);
  });

  test('Calculates 20% discount for $1000+ order', () async {
    registerMyNodes();

    final executor = DiscountCalculatorExecutor();
    final node = DiscountCalculatorNode(id: 'test_2');
    final context = MockExecutionContext({
      'amount': 1500,
    });

    final result = await executor.execute(node, context);

    expect(result.outputs['discountPercent'], 20);
    expect(result.outputs['finalPrice'], 1200);
  });
}

// Simple mock for testing
class MockExecutionContext extends ExecutionContext {
  final Map<String, dynamic> inputs;

  MockExecutionContext(this.inputs);

  @override
  T? getInput<T>(String portName) => inputs[portName] as T?;
}

Run tests:

dart test

Step 5: Use in a workflow

Once registered, your node appears in the workflow builder! Drag "Calculate Discount" into a workflow, connect an input (order amount), and it works.

Congratulations! You've built a production-ready custom node. 🎉


Creating Custom Nodes

Node Anatomy

Every custom node needs:

  1. Node Class - Defines inputs, outputs, and metadata
  2. Executor - Implements the node's logic
  3. Registration - Makes the node available

Step 1: Define the Node Class

import 'package:deepchain_sdk/deepchain_sdk.dart';

/// Sends data to a custom API endpoint.
///
/// This node makes HTTP requests to your custom service
/// and returns the response.
class CustomApiNode extends WorkflowNode {
  CustomApiNode({
    required super.id,
    super.position,
    super.data,
  }) : super(
    type: 'custom_api',
    name: 'Custom API',
    description: 'Calls your custom API endpoint',
    category: NodeCategory.integration,
    icon: 'api',

    // Define input ports
    inputPorts: [
      InputPort(
        name: 'endpoint',
        type: PortType.string,
        required: true,
        description: 'API endpoint path',
      ),
      InputPort(
        name: 'payload',
        type: PortType.object,
        required: false,
        description: 'Request body',
      ),
    ],

    // Define output ports
    outputPorts: [
      OutputPort(
        name: 'response',
        type: PortType.object,
        description: 'API response',
      ),
      OutputPort(
        name: 'error',
        type: PortType.object,
        description: 'Error details if request fails',
      ),
    ],

    // Node configuration schema
    configSchema: {
      'base_url': {
        'type': 'string',
        'required': true,
        'description': 'Base URL for the API',
      },
      'timeout': {
        'type': 'number',
        'default': 30000,
        'description': 'Request timeout in milliseconds',
      },
    },
  );

  @override
  CustomApiNode copyWith({
    String? id,
    NodePosition? position,
    Map<String, dynamic>? data,
  }) {
    return CustomApiNode(
      id: id ?? this.id,
      position: position ?? this.position,
      data: data ?? this.data,
    );
  }
}

Step 2: Create the Executor

import 'package:deepchain_sdk/deepchain_sdk.dart';
import 'package:http/http.dart' as http;

/// Executor for CustomApiNode.
class CustomApiExecutor extends NodeExecutor<CustomApiNode> {
  @override
  Future<NodeExecutionResult> execute(
    CustomApiNode node,
    ExecutionContext context,
  ) async {
    try {
      // Get inputs from context
      final endpoint = context.getInput<String>('endpoint');
      final payload = context.getInput<Map<String, dynamic>?>('payload');

      // Get configuration
      final baseUrl = node.config['base_url'] as String;
      final timeout = node.config['timeout'] as int? ?? 30000;

      // Make the API call
      final uri = Uri.parse('$baseUrl$endpoint');
      final response = await http.post(
        uri,
        body: payload != null ? jsonEncode(payload) : null,
        headers: {'Content-Type': 'application/json'},
      ).timeout(Duration(milliseconds: timeout));

      // Parse response
      final responseBody = jsonDecode(response.body);

      if (response.statusCode >= 200 && response.statusCode < 300) {
        return NodeExecutionResult.success(
          outputs: {
            'response': {
              'statusCode': response.statusCode,
              'body': responseBody,
              'headers': response.headers,
            },
          },
        );
      } else {
        return NodeExecutionResult.success(
          outputs: {
            'error': {
              'statusCode': response.statusCode,
              'message': responseBody['error'] ?? 'Request failed',
            },
          },
          outputPort: 'error',  // Route to error output
        );
      }
    } catch (e) {
      return NodeExecutionResult.failure(
        error: 'API call failed: $e',
        outputs: {
          'error': {'message': e.toString()},
        },
      );
    }
  }
}

Step 3: Register Node and Executor

// registration.dart
import 'package:deepchain_sdk/deepchain_sdk.dart';

void registerCustomNodes() {
  // Register node type
  NodeRegistry.register('custom_api', CustomApiNode.new);

  // Register executor
  ExecutorRegistry.register('custom_api', CustomApiExecutor());
}

Port Types

Type Dart Type Description
string String Text data
number num Integer or decimal
boolean bool True/false
object Map<String, dynamic> JSON object
array List JSON array
any dynamic Any type

Node Categories

enum NodeCategory {
  controlFlow,    // If, Switch, Loop, etc.
  data,           // Transform, Parse, Set
  integration,    // HTTP, APIs
  communication,  // Email, Slack, etc.
  ai,             // AI/ML nodes
  utility,        // Log, Delay, etc.
}

Creating Connectors

Connectors integrate with external services via their APIs.

Using OpenAPI Generator

The fastest way to create a connector:

# Generate from OpenAPI spec
dart run deepchain_sdk:generate_connector \
  --spec path/to/openapi.yaml \
  --output lib/connectors/my_service/

Manual Connector Creation

import 'package:deepchain_sdk/deepchain_sdk.dart';

/// Connector for MyService API.
class MyServiceConnector extends EnterpriseConnectorBase {
  @override
  String get id => 'my_service';

  @override
  String get name => 'My Service';

  @override
  String get description => 'Integration with My Service API';

  @override
  AuthType get authType => AuthType.oauth2;

  @override
  List<ConnectorOperation> get operations => [
    ConnectorOperation(
      id: 'get_users',
      name: 'Get Users',
      description: 'Retrieve list of users',
      method: HttpMethod.get,
      path: '/users',
      inputSchema: {
        'limit': {'type': 'number', 'default': 100},
        'offset': {'type': 'number', 'default': 0},
      },
    ),
    ConnectorOperation(
      id: 'create_user',
      name: 'Create User',
      description: 'Create a new user',
      method: HttpMethod.post,
      path: '/users',
      inputSchema: {
        'name': {'type': 'string', 'required': true},
        'email': {'type': 'string', 'required': true},
      },
    ),
  ];

  @override
  Future<void> authenticate(Credentials credentials) async {
    // Implement OAuth2 flow
  }

  @override
  Future<ConnectorResult> execute(
    String operation,
    Map<String, dynamic> params,
  ) async {
    final op = operations.firstWhere((o) => o.id == operation);
    // Execute the operation
  }
}

Register Connector

ConnectorRegistry.register(MyServiceConnector());

Publishing

Package Structure

my_deepchain_extension/
├── lib/
│   ├── my_extension.dart      # Main entry point
│   ├── nodes/
│   │   └── custom_api_node.dart
│   └── executors/
│       └── custom_api_executor.dart
├── pubspec.yaml
├── README.md
├── CHANGELOG.md
└── LICENSE

pubspec.yaml

name: my_deepchain_extension
description: Custom nodes for DeepChain
version: 1.0.0
homepage: https://github.com/you/my-extension
repository: https://github.com/you/my-extension

environment:
  sdk: ^3.0.0

dependencies:
  deepchain_sdk: ^0.1.0

Submit to Marketplace

  1. Ensure your package follows conventions
  2. Add comprehensive documentation
  3. Include tests
  4. Submit via DeepChain Marketplace portal

API Reference

WorkflowNode

Base class for all nodes.

abstract class WorkflowNode {
  final String id;
  final String type;
  final String name;
  final String description;
  final NodeCategory category;
  final List<InputPort> inputPorts;
  final List<OutputPort> outputPorts;
  final Map<String, dynamic> configSchema;
}

NodeExecutor

Base class for node execution.

abstract class NodeExecutor<T extends WorkflowNode> {
  Future<NodeExecutionResult> execute(T node, ExecutionContext context);
}

ExecutionContext

Provides access to workflow state.

class ExecutionContext {
  T? getInput<T>(String portName);
  void setOutput(String portName, dynamic value);
  String? getCredential(String key);
  Map<String, dynamic> get variables;
}

NodeExecutionResult

Result of node execution.

class NodeExecutionResult {
  factory NodeExecutionResult.success({
    required Map<String, dynamic> outputs,
    String? outputPort,
  });

  factory NodeExecutionResult.failure({
    required String error,
    Map<String, dynamic>? outputs,
  });
}

More Examples

  • API Integration — Make HTTP calls and transform responses
  • Database Query — Query SQL databases
  • Slack Notification — Send messages to Slack
  • Data Transformation — Parse JSON, manipulate data

See the examples directory for complete code.


Troubleshooting

Node doesn't appear in workflow builder

./deepchain update-nodes
./deepchain restart frontend

Executor not being called

Check that it's registered:

ExecutorRegistry.register('my_node_id', MyNodeExecutor());

Input/output not working

Verify port names match exactly:

// Must match what executor expects
InputPort(name: 'email', ...)  // In node definition
context.getInput<String>('email')  // In executor

Next Steps