Portal Community

The Dual-Role Architecture

In the standard model, a node reads data from upstream nodes and writes results to its output. DataStateMachine extends this: the node can be a system of record for its own entity, and NodeCapabilities lets it expose that entity to other nodes as a queryable service.

Single Node — Dual Role
─────────────────────────────────────────────────────────
                    ┌──────────────────────────────────┐
                    │  EnrichmentNode (CustomerData)   │
                    │                                  │
  Workflow Input ──►│  DataStateMachine (state + lock) │◄── TryAcquireLock / Set
                    │  CustomerDataHandler             │    (owns CRM_Customers table)
                    │                                  │
                    │  NodeCapability: "entity"        │◄── Other nodes query this
                    │  → GetCustomerAsync(customerId)  │    via capability protocol
                    └──────────────────────────────────┘

NodeCapabilities Overview

NodeCapabilities is a declaration system. A node declares what services it can provide (Webhook, Form, Entity, BusinessService, Datasource, Rules, Messaging, etc.). Other nodes in the same or different workflows can discover and call these capabilities without direct node-to-node coupling.

When combined with a DataStateMachine handler, the capability serves data that the node has already validated, deduped, and stored. The capability consumer always reads a current, consistent record.

Capability Type Description DataStateMachine Synergy
Entity Expose a business entity (customer, order, lead) Node owns entity via handler; capability serves it
Datasource Expose a queryable data source Node aggregates via handler; capability exposes query API
BusinessService Expose a domain service Node executes domain logic with idempotency; capability exposes result

Declaring an Entity Capability

// NodeCapabilityDeclaration — added to the node's capability manifest
public class CustomerEnrichmentNodeCapabilities : INodeCapabilityProvider
{
    public IEnumerable<NodeCapabilityDeclaration> GetCapabilities()
    {
        yield return new NodeCapabilityDeclaration
        {
            CapabilityType = NodeCapabilityType.Entity,
            Key            = "customer-data",
            DisplayName    = "Customer Data",
            Description    = "Provides enriched customer records owned by this node",
            Methods        = new[] { "GetCustomerAsync", "QueryCustomersAsync" }
        };
    }
}

Consuming the Capability from Another Node

// In another node's executor — consuming the capability
var customerCapability = await _capabilityResolver
    .ResolveAsync<IEntityCapability>("customer-data", tenantId, ct);

// Get customer data — served from the node's handler backing store
var customer = await customerCapability.GetAsync(customerId: ctx.Input.GetString("customerId"), ct);

// The capability returns whatever the CustomerEnrichmentNode last stored
// via DataStateMachine.SetAsync or UpsertAsync

How DataStateMachine and NodeCapabilities Interlock

Step Who Acts What Happens
1 CustomerEnrichmentNode runs in workflow DataStateMachine.TryAcquireLock → enrich → SetAsync(Processed, enrichedData)
2 Enriched record stored in CRM_Customers Handler writes/updates the customer row
3 Another workflow's node needs this customer Resolves "customer-data" NodeCapability
4 Capability calls CustomerEnrichmentNode's service Reads from CRM_Customers via handler repository
5 Returns consistent, validated, deduped record Consumer never touches the raw table — only the capability

Data Freshness and State Awareness

A capability consumer can check the DataStateMachine state before trusting the data. If the entity's status is Processing (being enriched now) or Failed (last enrichment failed), the consumer can decide whether to wait, use stale data, or skip.

// Capability consumer — state-aware read
var state = await customerCapability.GetStateAsync(customerId, ct);

if (state.Status == IdempotencyStatus.Processing)
    return NodeExecutionResult.Skip("Customer enrichment in progress — retry later");

if (state.Status == IdempotencyStatus.Failed)
    return NodeExecutionResult.Warn("Customer data may be stale — enrichment last failed");

var customer = await customerCapability.GetAsync(customerId, ct);
// proceed with customer data

This pattern eliminates the need for a separate data service layer for many use cases. The node IS the service. The DataStateMachine IS the consistency guarantee. NodeCapabilities IS the API contract.