Node as Entity Owner
A workflow node is not just a step — it can own a business entity. With a custom DataStateMachine handler, a node manages a database table, a REST API resource, or a blockchain ledger entry directly. State management becomes data management.
The Entity Ownership Concept
Traditional workflow engines treat the workflow as the unit of work. The node is just a step. State belongs to the engine.
DataStateMachine inverts this for custom handlers. The handler writes to YOUR table — not a platform log table. The node that uses that handler is the system of record for that entity type. It owns the entity.
Traditional model:
Workflow Engine → WorkflowNode → logs in engine tables
Entity ownership model:
Workflow Engine → WorkflowNode → [DataStateMachine handler]
│
▼
Your Domain Table
(Invoice, Patient, Lead, Transaction...)
Binding to a Database Table
Any SQL table can be a handler's backing store. The handler maps ItemKey and ItemEntityKey to the table's business key columns. The node becomes the table's primary writer during workflow execution.
| Node | Handler Table | ItemEntityKey | ItemKey | What the Node Manages |
|---|---|---|---|---|
| InvoiceDispatch | Invoice |
InvoiceNumber | "dispatch" |
Invoice dispatch status + response |
| KYCVerification | ComplianceRecord |
CustomerID | "kyc" |
KYC check result, score, verification date |
| PaymentSettle | Transaction |
TransactionRef | "settle" |
Settlement status, bank response, timestamp |
public class InvoiceHandlerRepository
{
public async Task<IdempotencyStatus?> GetCurrentStatusAsync(StateHandlerContext ctx, CancellationToken ct)
{
var invoice = await _ctx.Invoices
.Where(i => i.InvoiceNumber == ctx.ItemEntityKey
&& i.TenantID == ctx.TenantID
&& !i.Deleted)
.FirstOrDefaultAsync(ct);
return invoice is null ? null : (IdempotencyStatus)invoice.DispatchStatus;
}
}
Binding to a REST API
The handler doesn't have to use a database. It can wrap any REST API. TryAcquireLockAsync can write a "locked" flag to a remote system. SetAsync can call a PATCH endpoint to mark the resource complete.
public class ExternalComplianceHandler : IDataStateMachineHandler
{
public string HandlerKey => "compliance-api";
public async Task<bool> TryAcquireLockAsync(StateHandlerContext ctx, CancellationToken ct)
{
// POST to compliance API to reserve the slot
var response = await _httpClient.PostAsJsonAsync(
$"/api/records/{ctx.ItemEntityKey}/lock",
new { tenantId = ctx.TenantID, itemKey = ctx.ItemKey, expiresAt = DateTime.UtcNow.AddMinutes(30) },
ct);
return response.IsSuccessStatusCode;
}
public async Task SetAsync(StateHandlerContext ctx, IdempotencyStatus status, string? data, CancellationToken ct)
{
await _httpClient.PatchAsJsonAsync(
$"/api/records/{ctx.ItemEntityKey}",
new { status = status.ToString(), completedAt = DateTime.UtcNow, data },
ct);
}
// ... other methods
}
Binding to a Blockchain / DID Ledger
Blockchain integration is a natural fit for immutable state anchoring. The handler writes a state hash to a smart contract or DID registry. ExistsAsync queries the ledger to confirm the record. The node becomes the workflow's anchor point for immutable proof.
public class BlockchainStateHandler : IDataStateMachineHandler
{
public string HandlerKey => "ethereum-state";
public async Task<bool> ExistsAsync(StateHandlerContext ctx, CancellationToken ct)
{
// Query smart contract event log for this entity key
var events = await _contract.GetEventsByKeyAsync(
key: $"{ctx.ItemEntityKey}:{ctx.ItemKey}:{ctx.TenantID}",
ct: ct);
return events.Any(e => e.Status == "Processed");
}
public async Task SetAsync(StateHandlerContext ctx, IdempotencyStatus status, string? data, CancellationToken ct)
{
// Anchor state hash on-chain — immutable, timestamped
var stateHash = ComputeHash(ctx.ItemEntityKey, ctx.ItemKey, data);
await _contract.AnchorStateAsync(
key: $"{ctx.ItemEntityKey}:{ctx.ItemKey}:{ctx.TenantID}",
stateHash: stateHash,
status: status.ToString(),
ct: ct);
}
}
Entity Ownership Patterns
| Pattern | Handler Strategy | Example |
|---|---|---|
| Table-per-entity | Custom handler per domain entity | InvoiceHandler, KYCHandler, PaymentHandler |
| Shared business table | One handler, ItemEntityKey discriminates | LeadsHandler (email = entity, leadType = key) |
| API-backed entity | Handler wraps HTTP client | ComplianceApiHandler, CRMApiHandler |
| Immutable ledger | Handler writes to blockchain/DID | EthereumStateHandler, DIDRegistryHandler |
| IoT telemetry | Handler writes to time-series store | InfluxDBHandler, TimescaleHandler |
Why Entity Ownership Matters for Data Engineering
When the node owns the entity, every state transition is a data event. The workflow becomes a data pipeline. You get lineage, deduplication, and audit for free — without a separate ETL system.
The entity table is the source of truth. The workflow is the writer. The DataStateMachine is the concurrency guard. Together they are a complete data management system embedded in your workflow runtime.
Entity ownership works best when the node is the ONLY writer to the entity table during workflow execution. If other systems also write to the table, TryAcquireLockAsync must account for external writes — consider adding an optimistic concurrency token (row version) to your update logic.