Flow Studio
Building a Custom Guard
A custom guard implements IGuardRail and is registered in DI as IGuardRail. The guard infrastructure auto-discovers all IGuardRail registrations and calls them in the appropriate pipeline phase.
Complete Custom Guard Example
// DomainAllowlistGuard.cs — only allows execution if the email domain is approved
using BizFirst.Ai.GuardRails.Domain.Contracts;
using BizFirst.Ai.GuardRails.Domain.Enums;
using BizFirst.Ai.GuardRails.Domain.Models;
public class DomainAllowlistGuard : IGuardRail
{
// IGuardRailExecutor
public string Name => "DomainAllowlistGuard";
public string Version => "1.0.0";
public IReadOnlyList<GuardRailPhase> SupportedPhases =>
new[] { GuardRailPhase.Pre };
// IGuardRailDescriptor
public string GuardName => "Domain Allowlist Guard";
public string Description => "Blocks execution if the recipient email domain is not in the allowlist.";
public bool IsSecurityCritical => true;
public string ConfigurationSchema => @"{
""type"": ""object"",
""properties"": {
""allowedDomains"": {
""type"": ""array"",
""items"": { ""type"": ""string"" },
""description"": ""List of approved email domains (e.g. ['example.com'])""
}
},
""required"": [""allowedDomains""]
}";
public IReadOnlyList<GuardRailDependency> Dependencies =>
Array.Empty<GuardRailDependency>();
private IList<string> _allowedDomains = new List<string>();
public void SetConfiguration(IDictionary<string, object?> configuration)
{
if (configuration.TryGetValue("allowedDomains", out var domains)
&& domains is IList<object> list)
{
_allowedDomains = list.Select(d => d?.ToString() ?? "").ToList();
}
}
// IGuardRailExecutor.ExecuteAsync
public Task<GuardRailCheckResult> ExecuteAsync(
GuardRailExecutionContext context,
GuardRailPhase phase,
CancellationToken ct)
{
if (phase != GuardRailPhase.Pre)
return Task.FromResult(GuardRailCheckResult.Success());
var input = context.Input as IDictionary<string, object>;
var email = input?.TryGetValue("recipientEmail", out var e) == true
? e?.ToString() ?? "" : "";
var domain = email.Contains('@')
? email.Split('@')[1].ToLowerInvariant() : "";
if (!_allowedDomains.Contains(domain))
{
return Task.FromResult(GuardRailCheckResult.Blocked(
$"Email domain '{domain}' is not in the allowlist.",
metadata: new Dictionary<string, object?> { { "domain", domain } }));
}
return Task.FromResult(GuardRailCheckResult.Success());
}
// IGuardRailConfigValidator
public bool Validate(IDictionary<string, object?> configuration)
=> ValidateWithDetails(configuration).IsValid;
public GuardRailConfigValidationResult ValidateWithDetails(IDictionary<string, object?> configuration)
{
if (!configuration.ContainsKey("allowedDomains"))
return GuardRailConfigValidationResult.Invalid(new[] { "allowedDomains is required" });
return GuardRailConfigValidationResult.Valid();
}
}
Registering the Custom Guard
// In your INodeExecutorDependency.RegisterDefaults() or startup
services.AddScoped<IGuardRail, DomainAllowlistGuard>();
// The IGuardRailsExecutor resolves all IGuardRail registrations automatically
Testing Pattern
[Fact]
public async Task Guard_BlocksDisallowedDomain()
{
var guard = new DomainAllowlistGuard();
guard.SetConfiguration(new Dictionary<string, object?>
{
{ "allowedDomains", new List<object> { "example.com" } }
});
var ctx = new GuardRailExecutionContext
{
Input = new Dictionary<string, object>
{ { "recipientEmail", "alice@badactor.io" } }
};
var result = await guard.ExecuteAsync(ctx, GuardRailPhase.Pre);
Assert.False(result.IsAllowed);
Assert.Contains("badactor.io", result.ErrorMessage);
}
SetConfiguration Is Called Before ExecuteAsync
The guard infrastructure calls
SetConfiguration() with the Atlas Form values before the pipeline runs. Store configuration in instance fields after parsing. Do not read configuration inside ExecuteAsync.