Portal Community

Use Case Overview Table

# Use Case Industry Handler Key Benefit
1 Payment Idempotency Financial Services Built-in Prevent duplicate charges across retries
2 Loan Application Processing Financial Services Custom Track application state with full audit trail
3 Multi-Step KYC Approval Compliance Custom (Compliance API) Sub-item keys per check; terminal state blocks re-entry
4 ETL Pipeline Deduplication Data Engineering Built-in Node-level dedup prevents duplicate rows in data warehouse
5 SLA Tracking & Enforcement Operations Built-in Status transitions = SLA checkpoint log
6 Order Management E-commerce Custom (Order table) Node owns order status; ExistsAsync prevents double-dispatch
7 Blockchain State Anchoring Web3 / Compliance Custom (Blockchain) Immutable proof of processing on-chain
8 Regulatory Audit Trail Financial / Healthcare Built-in Append-only UpsertAsync = full processing history per entity
9 AI/ML Job Deduplication Data Science Built-in Prevent duplicate inference jobs for same input
10 IoT Telemetry Ingestion Manufacturing / Industrial Custom (Time-series) Per-device, per-reading idempotency at Element scope
11 CRM Lead Lifecycle Sales / Marketing SalesLeads handler Email+LeadType as keys; in-place CRM record management
12 HR Onboarding Workflow HR / Operations Custom (HR table) Step-by-step state: documents, IT access, training completion

1. Payment Idempotency

Payment APIs are inherently unreliable — network failures, gateway timeouts, and client disconnects all require retries. Without idempotency at the node level, each retry fires a new payment request, resulting in duplicate charges that generate chargebacks, customer complaints, and manual reconciliation work.

The DataStateMachine prevents this by calling TryAcquireLockAsync on the payment reference before ever touching the payment API. If the state is already Processed, the node skips immediately. If the state is Processing, another thread already holds the lock — skip again. Only a fresh record with no prior state proceeds to the payment API call, guaranteeing exactly-once execution regardless of retry count.

// Node configuration
handler: "built-in"
scope:   "Element"

// Node execution
var paymentRef = ctx.Input.GetString("paymentReference");

if (await DataStateMachine.ExistsAsync(paymentRef, entityKey: "payment", ct: ct))
    return NodeExecutionResult.Skip("Payment already processed");

bool locked = await DataStateMachine.TryAcquireLockAsync(paymentRef, entityKey: "payment", ct: ct);
if (!locked)
    return NodeExecutionResult.Skip("Payment processing in another thread");

// Call payment API exactly once
var result = await _paymentGateway.ChargeAsync(paymentRef, amount, ct);
await DataStateMachine.SetAsync(paymentRef, "payment", IdempotencyStatus.Processed,
    data: JsonSerializer.Serialize(result), ct: ct);

2. Loan Application Processing

Loan applications pass through multiple sequential stages: submission, credit check, underwriting, and approval. Each stage is its own workflow node, and each must be idempotent — a system restart mid-underwriting must resume from exactly where it stopped, not re-run stages that already completed.

A custom LoanApplicationHandler backed by the Loan_Applications table lets each node write its stage output directly to the business table. UpsertAsync after each stage creates an append-only history: every stage's output is preserved as an independent row linked by SupersededByID. Regulators and ops teams can reconstruct the complete decision chain for any loan.

// Handler mapping (in node settings)
handler:        "loan-application"   // custom handler name
itemEntityKey:  loanId               // e.g. "LOAN-2024-00417"
itemKey:        "credit-check"       // current stage name

// Each stage node writes its result
await DataStateMachine.UpsertAsync(
    itemKey:       loanId,
    itemEntityKey: "credit-check",
    data: JsonSerializer.Serialize(creditCheckResult),
    ct:   ct);
// Previous credit-check row → Status = Superseded (5)

3. Multi-Step KYC Approval

Know Your Customer (KYC) workflows run multiple parallel sub-checks: identity verification, address confirmation, PEP (politically exposed person) screening, and sanctions list lookup. Each sub-check is independent but belongs to the same customer entity. Tracking them as separate records without a shared idempotency boundary creates coordination gaps.

The SubItemKey field solves this cleanly. One node, one itemEntityKey (the customer ID), and one itemKey per check type. All sub-checks share the same Element scope, so the overall KYC lock and each sub-check lock are independently managed. A terminal Processed or Failed state on any sub-check blocks that sub-check from re-running without an explicit reset.

// Three sub-checks for the same customer — same entity, different sub-keys
var customerId = ctx.Input.GetString("customerId");

foreach (var check in new[] { "identity", "address", "pep-screen", "sanctions" })
{
    bool locked = await DataStateMachine.TryAcquireLockAsync(
        itemKey: customerId, itemEntityKey: "kyc",
        subItemKey: check, ct: ct);

    if (!locked) continue; // already done or in progress

    var result = await _complianceApi.RunCheckAsync(customerId, check, ct);
    await DataStateMachine.SetAsync(customerId, "kyc",
        subItemKey: check,
        status: result.Passed ? IdempotencyStatus.Processed : IdempotencyStatus.Failed,
        data: JsonSerializer.Serialize(result), ct: ct);
}

4. ETL Pipeline Deduplication

ETL pipelines re-run on schedules, after failures, and on demand. Without node-level deduplication, the same source record gets extracted, transformed, and loaded multiple times — filling the data warehouse with duplicate rows that corrupt analytics and require expensive dedup jobs to clean up.

The built-in handler at Element scope provides the dedup registry. Each ETL node calls ExistsAsync with the source record ID before doing any work. A Processed state means the node already handled this record — skip with no database writes. This dedup fires independently at each node, so a partial re-run after a failure only re-processes the records that actually failed.

// Built-in handler, Element scope — consistent across all ETL nodes
var sourceRecordId = ctx.Input.GetString("sourceRecordId");

if (await DataStateMachine.ExistsAsync(sourceRecordId, entityKey: "etl-record", ct: ct))
    return NodeExecutionResult.Skip($"Record {sourceRecordId} already loaded");

bool locked = await DataStateMachine.TryAcquireLockAsync(
    sourceRecordId, entityKey: "etl-record", ct: ct);
if (!locked)
    return NodeExecutionResult.Skip("Record loading in another thread");

// Transform and load
var row = _transformer.Transform(ctx.Input);
await _warehouse.InsertAsync(row, ct);
await DataStateMachine.SetAsync(sourceRecordId, "etl-record",
    IdempotencyStatus.Processed, ct: ct);

5. SLA Tracking & Enforcement

Operations teams need to know which workflow steps completed within SLA windows — and which did not. Traditionally this requires a separate SLA tracking system that listens to workflow events and records timestamps. With DataStateMachine, that system is already built: the state records are the SLA log.

Each UpsertAsync call stores the processing duration and result in the Data field alongside the Created timestamp. A single SQL query against Process_StateLogs identifies every entity that exceeded the SLA threshold. The status transitions — Processing at T+0, Processed at T+N — are the SLA checkpoint log. No additional instrumentation needed.

// Store timing data with the state record
var startedAt = DateTime.UtcNow;
// ... do the work ...
var duration = DateTime.UtcNow - startedAt;

await DataStateMachine.UpsertAsync(
    itemKey: workOrderId, itemEntityKey: "sla-step",
    data: JsonSerializer.Serialize(new {
        completedAt  = DateTime.UtcNow,
        durationMs   = duration.TotalMilliseconds,
        slaThresholdMs = 30_000,
        slaMet       = duration.TotalMilliseconds < 30_000
    }), ct: ct);

-- SLA breach query
SELECT ItemKey, ItemEntityKey, Created,
       JSON_VALUE(Data, '$.durationMs') AS DurationMs
FROM Process_StateLogs
WHERE ItemEntityKey = 'sla-step'
  AND CAST(JSON_VALUE(Data, '$.slaMet') AS BIT) = 0;

6. Order Management

E-commerce order dispatch is a classic exactly-once problem. A dispatch node sends an order to a fulfilment warehouse. If the node crashes after dispatch but before recording success, a naive retry sends the order twice — two shipments, one customer, one very bad day for the operations team.

A custom OrderHandler backed by the Orders table makes the node the single source of truth for order status. TryAcquireLockAsync prevents concurrent dispatch. SetAsync(Processed) writes the dispatched status directly to the Orders table. ExistsAsync on retry finds the Processed state and skips. The dispatch is exactly-once, and the Orders table always reflects reality.

// Node owns the Orders table — OrderHandler writes directly to it
var orderId = ctx.Input.GetString("orderId");

// Prevents double-dispatch, including concurrent threads
bool locked = await DataStateMachine.TryAcquireLockAsync(
    orderId, entityKey: "dispatch", ct: ct);

if (!locked)
{
    var existing = await DataStateMachine.GetAsync(orderId, "dispatch", ct: ct);
    if (existing?.Status == IdempotencyStatus.Processed)
        return NodeExecutionResult.Skip("Order already dispatched");
    return NodeExecutionResult.Skip("Dispatch in progress");
}

await _fulfilmentApi.DispatchAsync(orderId, ct);
await DataStateMachine.SetAsync(orderId, "dispatch",
    IdempotencyStatus.Processed, ct: ct);
// OrderHandler writes status = "Dispatched" to Orders table

7. Blockchain State Anchoring

Regulated industries — financial services, healthcare, supply chain — increasingly require immutable proof that a processing step occurred at a specific time. A database record can be modified. An on-chain transaction cannot. Blockchain anchoring makes the state record tamper-evident by design.

A custom BlockchainHandler replaces the SQL backing store with an on-chain ledger. TryAcquireLockAsync reserves a processing slot off-chain. SetAsync computes a hash of the state data and anchors it on-chain via IPFS + a blockchain timestamp. ExistsAsync queries the ledger to confirm prior anchoring. The on-chain record is final — no off-chain system can dispute or modify it.

// BlockchainHandler implements IDataStateMachineHandler
// ExistsAsync  → query on-chain by entityKey + itemKey hash
// SetAsync     → compute state hash, write IPFS CID, anchor on-chain
// GetAsync     → fetch and verify on-chain record

// Node usage is identical to the built-in handler
bool locked = await DataStateMachine.TryAcquireLockAsync(
    documentId, entityKey: "compliance-anchor", ct: ct);

var stateHash = ComputeHash(documentId, processingResult);
await DataStateMachine.SetAsync(
    documentId, "compliance-anchor",
    IdempotencyStatus.Processed,
    data: JsonSerializer.Serialize(new { hash = stateHash, cid = ipfsCid }),
    ct: ct);
// BlockchainHandler writes: (entityKey, itemKey, stateHash, timestamp) → blockchain

8. Regulatory Audit Trail

Financial and healthcare workflows must demonstrate a complete, tamper-evident record of every processing decision — who processed what, when, with what result, and in what sequence. Maintaining a separate audit database alongside the operational database doubles infrastructure complexity and creates synchronisation risk.

UpsertAsync after every state change creates a new append-only row. The SupersededByID column links the history chain, so each row points to the row it replaced. The Process_StateLogs table is the audit trail — no separate system required. Regulators can query the table directly or via API to reconstruct the complete decision history for any entity at any point in time.

-- Regulatory audit query: full processing history for one entity
SELECT
    sl.ID,
    sl.Status,
    sl.ItemKey          AS ProcessingStage,
    sl.ItemEntityKey    AS EntityID,
    sl.Data             AS DecisionData,
    sl.Created          AS DecisionTimestamp,
    sl.SupersededByID   AS ReplacedBy
FROM Process_StateLogs sl
WHERE sl.ItemEntityKey = 'CLAIM-2024-00183'
  AND sl.TenantID      = 42
ORDER BY sl.Created ASC;
-- Returns: complete append-only decision chain, oldest to newest
-- Each row shows what was decided, when, and what decision it superseded

9. AI/ML Job Deduplication

AI inference jobs — document classification, image analysis, customer risk scoring — are expensive in compute time and API cost. When the same workflow runs multiple times (scheduled re-runs, manual retries, duplicate triggers), the same input should not be re-inferred. The result from the first run should be reused.

The built-in handler stores the inference output as JSON in the Data field. On subsequent workflow runs, ExistsAsync returns true, the node retrieves the cached result from GetAsync, and skips the API call entirely. Cache hit rate directly reduces AI compute costs. The result is still fresh from the node's perspective — the downstream workflow receives the same output as if inference ran again.

var inputHash = ComputeStableHash(ctx.Input.GetString("document"));

var cached = await DataStateMachine.GetAsync(inputHash, "inference-job", ct: ct);
if (cached?.Status == IdempotencyStatus.Processed)
{
    ctx.Output.Set("inferenceResult", cached.Data);  // return cached result
    return NodeExecutionResult.Success("Cache hit — inference reused");
}

bool locked = await DataStateMachine.TryAcquireLockAsync(
    inputHash, entityKey: "inference-job", ct: ct);
if (!locked)
    return NodeExecutionResult.Skip("Inference running in another thread");

var result = await _llmClient.InferAsync(ctx.Input, ct);
await DataStateMachine.SetAsync(inputHash, "inference-job",
    IdempotencyStatus.Processed, data: JsonSerializer.Serialize(result), ct: ct);
ctx.Output.Set("inferenceResult", JsonSerializer.Serialize(result));

10. IoT Telemetry Ingestion

IoT sensors frequently retransmit readings due to network instability, edge buffering, and delivery-guarantee protocols. A temperature sensor might send the same reading three or four times before receiving an acknowledgement. Without node-level deduplication, each retransmit inserts a duplicate row into the time-series database, corrupting aggregates and alerting logic.

A custom TimeSeriesHandler backed by InfluxDB or TimescaleDB provides per-device, per-reading idempotency. The exact reading is identified by deviceId (itemEntityKey), readingTimestamp (itemKey), and sensorType (subItemKey). High-throughput scenarios use App or Thread scope so device-level deduplication fires without crossing tenant boundaries.

// High-throughput IoT dedup — Thread scope, custom TimeSeriesHandler
var deviceId          = ctx.Input.GetString("deviceId");
var readingTimestamp  = ctx.Input.GetString("readingTimestamp");  // ISO 8601 exact
var sensorType        = ctx.Input.GetString("sensorType");        // "temperature", "pressure"

if (await DataStateMachine.ExistsAsync(
        itemKey: readingTimestamp,
        itemEntityKey: deviceId,
        subItemKey: sensorType, ct: ct))
    return NodeExecutionResult.Skip("Duplicate reading — already ingested");

bool locked = await DataStateMachine.TryAcquireLockAsync(
    readingTimestamp, deviceId, subItemKey: sensorType, ct: ct);
if (!locked) return NodeExecutionResult.Skip("Ingestion in progress");

await _timeSeriesDb.WriteAsync(deviceId, sensorType, readingTimestamp, value, ct);
await DataStateMachine.SetAsync(readingTimestamp, deviceId,
    subItemKey: sensorType, status: IdempotencyStatus.Processed, ct: ct);

11. CRM Lead Lifecycle

A sales lead flows through multiple workflow stages: capture, qualification, enrichment, assignment, and engagement. Each stage is a separate workflow node. Without idempotency, a lead that triggers the workflow twice (duplicate form submission, webhook retry) gets double-captured, double-assigned, and double-engaged — burning sales rep time on phantom leads.

The SalesLeads handler is purpose-built for this pattern. Email address is ItemEntityKey, stage name is ItemKey. The handler uses an in-place update strategy that keeps one row per lead per stage in the CRM table. Current state is always visible at a glance. The workflow manages transitions; the CRM table holds the outcome. No code changes required — the SalesLeads demo handler works directly.

// SalesLeads handler — maps directly to the CRM demo
handler:       "salesleads"        // IDataStateMachineHandler implementation
itemEntityKey: email               // "jane.smith@example.com"
itemKey:       "qualify"           // current workflow stage

// Qualification node
if (await DataStateMachine.ExistsAsync(email, "qualify", ct: ct))
    return NodeExecutionResult.Skip("Lead already qualified");

bool locked = await DataStateMachine.TryAcquireLockAsync(email, "qualify", ct: ct);
// SalesLeads handler: TryAcquireLockAsync → UPDATE SalesLeads SET Status = Processing
//                    SetAsync(Processed)  → UPDATE SalesLeads SET Status = Processed
var score = await _leadScorer.ScoreAsync(email, ct);
await DataStateMachine.SetAsync(email, "qualify",
    IdempotencyStatus.Processed,
    data: JsonSerializer.Serialize(new { score, tier = score > 80 ? "hot" : "warm" }),
    ct: ct);

12. HR Onboarding Workflow

HR onboarding is a multi-track parallel process: IT access provisioning, building access cards, document signing, benefits enrollment, and training assignment all run simultaneously. Each track is independent — IT access does not wait for document signing. But each track must be tracked, idempotent, and independently retryable without re-running completed tracks.

A custom HROnboardingHandler backed by the HR_OnboardingTasks table gives each track its own state. employeeId is itemEntityKey, track name is itemKey (e.g. "it-access", "building-access", "documents"). Each onboarding node at Element scope independently tracks completion. A dashboard query shows which tracks are complete, in-progress, or failed for every employee at a glance.

// HR Onboarding — one node per track, same employee, independent state
var employeeId = ctx.Input.GetString("employeeId");
var track      = "it-access";  // each node has its own track name

if (await DataStateMachine.ExistsAsync(employeeId, track, ct: ct))
    return NodeExecutionResult.Skip($"Track '{track}' already complete for {employeeId}");

bool locked = await DataStateMachine.TryAcquireLockAsync(employeeId, track, ct: ct);
if (!locked) return NodeExecutionResult.Skip("Track provisioning in progress");

await _itProvisioning.CreateAccountAsync(employeeId, ct);
await DataStateMachine.SetAsync(employeeId, track,
    IdempotencyStatus.Processed,
    data: JsonSerializer.Serialize(new { provisionedAt = DateTime.UtcNow }),
    ct: ct);

-- Dashboard: onboarding status per employee
SELECT ItemKey AS Track, Status, Created AS CompletedAt
FROM Process_StateLogs
WHERE ItemEntityKey = 'EMP-2024-00512' AND Deleted = 0
ORDER BY Created;