Portal Community

The Bring Your Own Design

The platform ships two handler implementations — built-in (SQL) and sales-leads (CRM demo). But the engine itself has zero compile-time dependency on either. The handler is resolved at runtime by a string key declared in the node's configuration block.

This means a node that was built and deployed six months ago can be pointed at a brand-new handler — one that writes to Redis, a blockchain ledger, or a third-party compliance API — without changing a single line of the node's code. The node never knows which handler it's talking to.

The Plugin Interface

// Ship this in your plugin assembly
public interface IDataStateMachinePlugin
{
    void Register(IServiceCollection services, IConfiguration config);
}

// Example implementation
public class MyCustomDataStateMachinePlugin : IDataStateMachinePlugin
{
    public void Register(IServiceCollection services, IConfiguration config)
    {
        // Register your handler as Singleton (handlers are shared across requests)
        services.AddSingleton<IDataStateMachineHandler, MyCustomHandler>();

        // Register any scoped dependencies your handler needs
        services.AddScoped<IMyCustomRepository, MyCustomRepository>();
    }
}

Handler Discovery — AppDomain Scan

At application startup, DataStateMachinePluginLoader scans every assembly in the AppDomain for types that implement IDataStateMachinePlugin. It instantiates each one and calls Register(services, config). No manual wiring in Program.cs required — drop the DLL in the output folder and it is picked up.

Application Startup
    │
    ▼
DataStateMachinePluginLoader.LoadPlugins(services, config)
    │
    ├── AppDomain.CurrentDomain.GetAssemblies()
    │       filter: types implementing IDataStateMachinePlugin
    │
    ├── BuiltInDataStateMachinePlugin.Register(...)
    │       → registers BuiltInDataStateMachineHandler (Singleton)
    │       → registers IProcessStateLogRepository (Scoped)
    │
    ├── SalesLeadsDataStateMachinePlugin.Register(...)
    │       → registers SalesLeadsDataStateMachineHandler (Singleton)
    │       → registers ISalesLeadRepository (Scoped)
    │
    └── YourCustomPlugin.Register(...)
            → registers whatever your handler needs

Implementing a Handler

public class MyCustomHandler : IDataStateMachineHandler
{
    public string HandlerKey => "my-custom";  // matches "handler": "my-custom" in node config

    public Task<bool> ExistsAsync(StateHandlerContext ctx, CancellationToken ct)
    {
        // ctx.ItemKey, ctx.ItemEntityKey, ctx.SubItemKey, ctx.SubItemEntityKey
        // ctx.TenantID, ctx.EntityID, ctx.StateLogScopeID
        // Query YOUR store using these keys
    }

    public Task<bool> TryAcquireLockAsync(StateHandlerContext ctx, CancellationToken ct) { ... }
    public Task SetAsync(StateHandlerContext ctx, IdempotencyStatus status, string? data, CancellationToken ct) { ... }
    public Task UpsertAsync(StateHandlerContext ctx, string? data, CancellationToken ct) { ... }
    public Task ReleaseLockAsFailedAsync(StateHandlerContext ctx, string? errorDetail, CancellationToken ct) { ... }
    public Task<StateRecord?> GetAsync(StateHandlerContext ctx, CancellationToken ct) { ... }
    public Task<IReadOnlyList<StateRecord>> QueryAsync(StateHandlerContext ctx, StateQuery query, CancellationToken ct) { ... }
}

Handler vs Plugin vs Factory — Responsibilities

Component Lifetime Responsibility
IDataStateMachinePlugin Called once at startup Registers handler and its dependencies into DI
IDataStateMachineHandler Singleton Executes all state operations against your backing store
DataStateMachineFactory Scoped (per request) Resolves handler by key; builds StateHandlerContext from node config + execution context
ContextualDataStateMachine Per node execution Wraps handler + base context; builds per-call ctx using C# with
IDataStateMachine Per node execution The interface the node developer calls — never sees the handler directly

Flexibility Matrix

Backing Store Example Use Case Custom Handler?
SQL Server (built-in) Standard workflow idempotency No — built-in handler
CRM Table Lead/contact state tracking Yes — SalesLeads demo as template
Redis High-throughput ephemeral locks Yes — implement IDataStateMachineHandler
REST API Third-party compliance record Yes
Blockchain / DID Ledger Immutable state anchoring Yes
Custom SQL table Domain-specific entity state Yes

3-Project Structure Per Handler

Every handler follows a 3-project pattern — Domain, Infrastructure, Service — matching the rest of the BizFirstAI architecture. This ensures clean separation and testability.

BizFirst.Ai.DataStateMachine.{Name}.Domain
    Entities\          ← EF Core entity mapping to your table
    Repositories\      ← IYourRepository interface
    Services\          ← IYourService interface

BizFirst.Ai.DataStateMachine.{Name}.Infrastructure
    Data\              ← DbContext — your EF Core tables
    Repositories\      ← EF implementation
    DependencyInjection.cs

BizFirst.Ai.DataStateMachine.{Name}.Service
    Services\          ← Business logic, Guard validations
    Handlers\          ← IDataStateMachineHandler implementation  ← KEY FILE
    {Name}DataStateMachinePlugin.cs  ← IDataStateMachinePlugin
    DependencyInjection.cs
Tip: The SalesLeads handler (Guide 06) is the canonical template for building a custom handler. It demonstrates the entity key mapping strategy, the in-place vs append-only update decision, and the plugin registration pattern.