Portal Community

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.