Portal Community

The Problem It Solves

Workflow engines orchestrate steps efficiently but treat state as a byproduct — logs belong to the engine, not your business. When you need "did this entity already get processed in the last 72 hours?", you write custom code. Every time. For every node. DataStateMachine makes per-node, per-entity state tracking a first-class platform feature.

Architecture Overview

Node PreProcess
    │
    ▼
DataStateMachineFactory (Scoped DI)
    │  resolves handler by HandlerKey from node config
    ▼
IDataStateMachineHandler  ←── plugin assembly (built-in, sales-leads, or custom)
    │
    ▼
ContextualDataStateMachine
    │  wraps base context + per-call item keys via C# `with`
    ▼
IDataStateMachine  ←── what the node developer calls
    │
    ├── TryAcquireLockAsync(itemKey, ...)   → locks entity for processing
    ├── SetAsync(itemKey, Processed, data)  → marks complete
    ├── UpsertAsync(itemKey, data)          → append-only record with SupersededByID
    └── ExistsAsync / GetAsync / QueryAsync → read state

Three Modes of Operation

Mode Config Behaviour
Built-In Handler handler: "built-in" Writes to Process_StateLogs SQL table. Full idempotency, locks, history.
Custom Handler handler: "your-key" Your plugin assembly. Any backing store — DB, API, blockchain, Redis.
No-Op (Null) (config block absent) NullDataStateMachine.Instance — safe default, no DB writes, safe for parallel fork lanes.

What a Node Developer Sees

// In your node executor — IDataStateMachine is pre-bound to scope/entity/tenant
var exists = await DataStateMachine.ExistsAsync(invoice.Id, entityKey: "invoice");
if (exists) return NodeExecutionResult.Skip("Already processed");

bool locked = await DataStateMachine.TryAcquireLockAsync(invoice.Id, entityKey: "invoice");
if (!locked) return NodeExecutionResult.Skip("Being processed by another thread");

try {
    await ProcessInvoiceAsync(invoice);
    await DataStateMachine.SetAsync(invoice.Id, entityKey: "invoice",
        status: IdempotencyStatus.Processed, data: result);
}
catch (Exception ex) {
    await DataStateMachine.ReleaseLockAsFailedAsync(invoice.Id, entityKey: "invoice", error: ex.Message);
    throw;
}

Guide Pages

# Page What You'll Learn
1 Plugin Architecture How to bring your own handler; BYOD concept; plugin discovery
2 Life Cycle Complete state machine lifecycle; annotated invoice walkthrough
3 Key Concepts Scope hierarchy, StateHandlerContext, IdempotencyStatus enum
4 Node Configuration JSON config block; handler/scope/options; NullDataStateMachine default
5 Built-In Handler Process_StateLogs schema; handler code; lock/unlock pattern
6 SalesLeads Demo Custom handler walkthrough; CRMSimple_SalesLeads table
7 Node as Entity Owner Binding nodes to DB tables, REST APIs, blockchain
8 NodeCapabilities Node as DataStateMachine + DataService provider
9 NodePolicies Security enforcement; handler/scope access control
10 GuardRails Integration State-driven validation; custom guard engines
11 Business Use Cases 12 real-world scenarios across industries
12 Data Engineering Lineage, dedup, quality gates, event sourcing
13 Data Science & Enrichment ML labels, enrichment pipelines, feature stores
Tip: If the dataStateMachine config block is absent from a node, NullDataStateMachine.Instance is injected automatically — a no-op singleton that never writes to the database. No configuration is required to use the safe default.