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
- Installation
- Build Your First Custom Node — Complete tutorial
- Advanced: Creating Connectors
- Publishing to Marketplace
- API Reference
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:
- Node Class - Defines inputs, outputs, and metadata
- Executor - Implements the node's logic
- 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
- Ensure your package follows conventions
- Add comprehensive documentation
- Include tests
- 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
- API Reference: REST API
- CLI: Use the CLI
- Deployment: Deploy custom nodes
- Publish: Share on Marketplace