Portal Community

One Attribute, Zero Registration

The [NodeCapability] attribute on an executor class is all that is needed. The engine scans all registered executor types at startup and reads the attribute to populate the CapabilityRegistry. No manual registry call is required.

Example Declarations

// Webhook capability
[NodeCapability(CapabilityType.Webhook)]
public class WebhookCallExecutor : BaseNodeExecutor { /* ... */ }

// Messaging capability — Slack
[NodeCapability(CapabilityType.Messaging)]
public class SlackMessageExecutor : BaseNodeExecutor { /* ... */ }

// Entity capability
[NodeCapability(CapabilityType.Entity)]
public class EntityReadExecutor : BaseNodeExecutor { /* ... */ }

// MCP capability
[NodeCapability(CapabilityType.MCP)]
public class MCPToolCallExecutor : BaseNodeExecutor { /* ... */ }

Multiple Capabilities (Rare)

In unusual cases, a node may bridge two capability domains. While the attribute is not marked AllowMultiple in the base design, the registry supports multiple capability associations per executor type via convention: add the node type to both capabilities' lookup lists manually if needed.

// A node that both calls a business service AND sends a message
// Primary capability declared via attribute:
[NodeCapability(CapabilityType.BusinessService)]
public class OrderConfirmationExecutor : BaseNodeExecutor
{
    // This executor calls the order service (BusinessService)
    // AND sends a confirmation email (Messaging)
    // but is categorized under BusinessService for palette grouping
}

Node Type Registration

Executor classes are registered with DI using a keyed registration pattern — the key is the node type string (matching the canvas node's type property):

// Registration in DI
services.AddKeyedScoped<INodeExecutor, SlackMessageExecutor>("SlackMessage");
services.AddKeyedScoped<INodeExecutor, EntityReadExecutor>("EntityRead");
services.AddKeyedScoped<INodeExecutor, MCPToolCallExecutor>("MCPToolCall");

Auto-Discovery at Startup

// ProcessEngine/Capabilities/CapabilityRegistry.cs (startup scan)
public void Discover(IEnumerable<Type> executorTypes)
{
    foreach (var type in executorTypes)
    {
        var attr = type.GetCustomAttribute<NodeCapabilityAttribute>();
        if (attr != null)
        {
            _byType[attr.Type].Add(type);
            _byExecutorType[type] = attr.Type;
        }
    }
}
Convention: Place all executor classes for a given capability in a dedicated subfolder — e.g., ExecutionNodes/Messaging/, ExecutionNodes/Entities/. This makes capability discovery predictable and keeps the codebase organized.