Portal Community

GuardRailExecutionContext Shape

// BizFirst.Ai.GuardRails.Domain.Models.GuardRailExecutionContext
public class GuardRailExecutionContext
{
    // Multi-tenancy isolation — use for scoped rate limits, quotas
    public long TenantId    { get; set; }

    // The user who triggered the execution (0 if system/scheduled)
    public long UserId      { get; set; }

    // The node's ProcessElementKey — unique identifier for this node in the workflow
    public string OperationId { get; set; }

    // Pre-phase: the input data arriving at this node
    // Post-phase: the output data produced by the executor
    public object? Input    { get; set; }
    public object? Output   { get; set; }

    // Additional metadata set by BaseNodeExecutor:
    //   "element_key"    → ProcessElementKey
    //   "executor_type"  → executor class name
    public IDictionary<string, object?> Metadata { get; set; }

    // Distributed trace ID for correlation (OpenTelemetry)
    public string TraceId   { get; set; }

    // "Pre" or "Post" (set by the pipeline stage)
    public string PhaseId   { get; set; }

    // When the context was created
    public DateTime CreatedAt { get; set; }
}

How the Context Is Built

The context is built in BaseNodeExecutor.OnPreGuardRails from the ProcessElementExecutionContext:

var guardContext = new GuardRailExecutionContext
{
    TenantId    = (long)executionContext.TenantId,
    UserId      = (long)executionContext.UserId,
    OperationId = executionContext.ProcessElementKey ?? "unknown",
    Input       = executionContext.InputData,
    TraceId     = "from-open-telemetry-activity",
    PhaseId     = "Pre",
    Metadata    = new Dictionary<string, object?>
    {
        { "element_key",   executionContext.ProcessElementKey },
        { "executor_type", this.GetType().Name                }
    }
};

Accessing Context in Your Guard

public async Task<GuardRailCheckResult> ExecuteAsync(
    GuardRailExecutionContext context,
    GuardRailPhase phase,
    CancellationToken ct)
{
    if (phase != GuardRailPhase.Pre)
        return GuardRailCheckResult.Success();

    // Scope rate limit per tenant
    var tenantId = context.TenantId;
    var nodeKey  = context.OperationId;

    // Access the input payload
    var inputDict = context.Input as Dictionary<string, object>;

    // Access custom metadata
    var executorType = context.Metadata.TryGetValue("executor_type", out var et)
        ? et?.ToString() ?? "unknown"
        : "unknown";

    // Your check logic
    var allowed = await CheckRateLimitAsync(tenantId, nodeKey, ct);
    return allowed
        ? GuardRailCheckResult.Success()
        : GuardRailCheckResult.Blocked("Rate limit exceeded", retryAfterSeconds: 30);
}

Post-Phase Output Access

In the Post phase, context.Output is populated with the executor's NodeExecutionResult.OutputData. Guards can inspect or even sanitize the output:

// Post-phase PII detection example
if (phase == GuardRailPhase.Post)
{
    var output = context.Output as Dictionary<string, object>;
    if (output != null && ContainsPii(output))
    {
        RedactPii(output);  // mutate the dictionary in-place
        return GuardRailCheckResult.Success(outputModified: true);
    }
}