Nodes as DataService Providers
A node that owns its primary entity via a custom DataStateMachine handler can also expose that data to other nodes through NodeCapabilities. The same node becomes both a state machine and a data service — without any additional infrastructure.
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.