Portal Community

What GuardRails Are

GuardRails are pluggable validation engines that the platform runs before (pre-guard) and after (post-guard) a node's ExecuteAsync. Each guard engine implements IGuardRailEngine and returns a GuardResult (Pass, Warn, Block).

Out of the box, GuardRails handle PII detection, rate limiting, circuit breaking, and input validation. The DataStateMachine adds a new category: stateful guards — validations that consult the entity's current state history before deciding.

Stateful Guard Pattern

A stateful guard queries the DataStateMachine before the node executes. If the entity is already in a terminal state (Processed, Skipped), it blocks re-entry. If the state shows repeated failures, it can apply a circuit-breaker rule.

Node Execution Request
    │
    ▼
Pre-Guard Pipeline
    ├── PiiGuard (built-in)
    ├── RateLimitGuard (built-in)
    └── TerminalStateGuard ◄──── reads DataStateMachine state
            │
            ├── ExistsAsync(itemKey) → true → GuardResult.Block("Already Processed")
            ├── GetAsync → Status = Failed, FailCount ≥ 3 → GuardResult.Block("Max retries exceeded")
            └── Status = None or Failed → GuardResult.Pass → node executes

Node.ExecuteAsync(...)
    │
    ▼
Post-Guard Pipeline
    └── DataQualityGuard ◄──── validates output before SetAsync is called

Implementing a Terminal State Guard

public class TerminalStateGuard : IGuardRailEngine
{
    private readonly IDataStateMachine _dsm;

    public TerminalStateGuard(IDataStateMachine dsm) { _dsm = dsm; }

    public async Task<GuardResult> EvaluateAsync(
        GuardContext ctx, CancellationToken ct)
    {
        var itemKey       = ctx.NodeInput.GetString("entityId");
        var itemEntityKey = ctx.NodeInput.GetString("entityType");

        // Block if already in terminal state
        if (await _dsm.ExistsAsync(itemKey, itemEntityKey, ct: ct))
            return GuardResult.Block(
                $"Entity {itemEntityKey}:{itemKey} is already in a terminal state. Processing skipped.");

        // Check failure count — circuit breaker
        var records = await _dsm.QueryAsync(
            itemKey, itemEntityKey,
            new StateQuery { StatusFilter = IdempotencyStatus.Failed },
            ct);

        if (records.Count >= 3)
            return GuardResult.Block(
                $"Entity {itemEntityKey}:{itemKey} has failed 3 times. Manual review required.");

        return GuardResult.Pass();
    }
}

Custom GuardRails Built on DataStateMachines

Because DataStateMachine handlers are pluggable, you can build domain-specific guards that consult your own entity tables. A compliance guard can check whether a customer's KYC record is current before allowing a payment node to run. A healthcare guard can verify that a patient's consent record is active before allowing data enrichment.

public class KycComplianceGuard : IGuardRailEngine
{
    private readonly IEntityCapability _kycCapability;

    public async Task<GuardResult> EvaluateAsync(GuardContext ctx, CancellationToken ct)
    {
        var customerId = ctx.NodeInput.GetString("customerId");

        // Query customer's KYC state via NodeCapability
        var kycState = await _kycCapability.GetStateAsync(customerId, ct);

        return kycState?.Status switch
        {
            IdempotencyStatus.Processed => GuardResult.Pass(),
            IdempotencyStatus.Processing => GuardResult.Warn("KYC verification in progress"),
            IdempotencyStatus.Failed =>
                GuardResult.Block("KYC verification failed — payment blocked"),
            null =>
                GuardResult.Block("No KYC record found — customer not verified"),
            _ => GuardResult.Block("KYC status does not permit payment")
        };
    }
}

GuardRails as Custom DataStateMachine Engines

This is the pattern described in the product concept: GuardRails can themselves be backed by a DataStateMachine. The guard reads state via one handler and writes its verdict via another. This enables:

Use Case Guard Reads Guard Writes
Anti-fraud rate gate Fraud score state (Redis handler) Rate gate state (built-in handler)
Compliance window KYC state (compliance API handler) Compliance gate log (built-in handler)
Content moderation Prior moderation results (built-in) Moderation decision (custom table handler)
SLA enforcement Processing timestamps (built-in) SLA breach log (analytics handler)

Registering a Stateful Guard

// In your plugin or startup
services.AddScoped<IGuardRailEngine, TerminalStateGuard>();
services.AddScoped<IGuardRailEngine, KycComplianceGuard>();

// In node configuration
{
  "guardRails": {
    "preGuards":  ["TerminalStateGuard", "KycComplianceGuard"],
    "postGuards": ["DataQualityGuard"]
  },
  "dataStateMachine": {
    "handler": "built-in",
    "scope":   "Element"
  }
}

Stateful guards and the node's DataStateMachine can share the same handler. The guard reads state in the pre-guard phase; the node writes state in ExecuteAsync. Both use the same ContextualDataStateMachine — same backing store, same consistency guarantee.

Pre-guards run BEFORE the processing lock is acquired. If multiple concurrent requests reach the guard simultaneously, both may pass (entity not yet in Processing state). The TryAcquireLockAsync call in the node body is the true concurrency gate — guards add domain logic, not lock semantics.