This guide explains how to create custom plugins for the Loopai framework.
Loopai provides three types of plugins for extensibility:
- Validator Plugins - Custom validation logic for execution results
- Sampler Plugins - Custom sampling strategies for quality assurance
- Webhook Handler Plugins - Custom webhook event processing
All plugins implement the IPlugin base interface and register with the IPluginRegistry.
Validates execution results against expected outputs or custom business rules.
Use Cases:
- Custom schema validation
- Business rule verification
- Domain-specific output validation
- Multi-field constraint checking
Interface:
public interface IValidatorPlugin : IPlugin
{
Task<ValidationResult> ValidateAsync(
ExecutionRecord execution,
ValidationContext context,
CancellationToken cancellationToken = default);
}Example Implementation:
using Loopai.Core.Plugins;
using Loopai.Core.Models;
public class CustomBusinessRuleValidator : IValidatorPlugin
{
public string Name => "custom-business-rule-validator";
public string Description => "Validates output against custom business rules";
public string Version => "1.0.0";
public string Author => "Your Name";
public bool IsEnabled { get; set; } = true;
public int Priority { get; set; } = 100;
public Task<ValidationResult> ValidateAsync(
ExecutionRecord execution,
ValidationContext context,
CancellationToken cancellationToken = default)
{
// Your validation logic here
var isValid = /* custom validation */;
return Task.FromResult(new ValidationResult
{
IsValid = isValid,
ValidatorType = Name,
Message = isValid ? "Valid" : "Invalid business rule",
ConfidenceScore = isValid ? 1.0 : 0.0
});
}
}Determines which executions should be sampled for validation.
Use Cases:
- Time-based sampling
- Error-rate triggered sampling
- Load-based sampling
- Pattern-based sampling
Interface:
public interface ISamplerPlugin : IPlugin
{
Task<SamplingDecision> ShouldSampleAsync(
ExecutionRecord execution,
SamplingContext context,
CancellationToken cancellationToken = default);
}Example Implementation:
using Loopai.Core.Plugins;
using Loopai.Core.Models;
public class ErrorRateSamplerPlugin : ISamplerPlugin
{
private readonly double _errorThreshold;
public ErrorRateSamplerPlugin(double errorThreshold = 0.05)
{
_errorThreshold = errorThreshold;
}
public string Name => "error-rate-sampler";
public string Description => $"Samples when error rate exceeds {_errorThreshold * 100}%";
public string Version => "1.0.0";
public string Author => "Your Name";
public bool IsEnabled { get; set; } = true;
public int Priority { get; set; } = 50;
public Task<SamplingDecision> ShouldSampleAsync(
ExecutionRecord execution,
SamplingContext context,
CancellationToken cancellationToken = default)
{
// Calculate error rate from context
var errorRate = /* calculate from execution history */;
var shouldSample = errorRate > _errorThreshold;
return Task.FromResult(new SamplingDecision
{
ShouldSample = shouldSample,
Reason = $"Error rate: {errorRate:P2}",
SamplerType = Name,
ConfidenceScore = shouldSample ? 1.0 : 0.0
});
}
}Processes webhook events for integrations and notifications.
Use Cases:
- Slack/Teams notifications
- External system integration
- Email alerts
- Custom analytics
Interface:
public interface IWebhookHandlerPlugin : IPlugin
{
IEnumerable<WebhookEventType> SupportedEvents { get; }
Task HandleAsync(
WebhookEvent webhookEvent,
WebhookHandlerContext context,
CancellationToken cancellationToken = default);
bool ValidateConfiguration(IReadOnlyDictionary<string, object> configuration);
}Example Implementation:
using Loopai.Core.Plugins;
public class SlackWebhookHandlerPlugin : IWebhookHandlerPlugin
{
public string Name => "slack-webhook-handler";
public string Description => "Sends notifications to Slack";
public string Version => "1.0.0";
public string Author => "Your Name";
public bool IsEnabled { get; set; } = true;
public int Priority { get; set; } = 100;
public IEnumerable<WebhookEventType> SupportedEvents => new[]
{
WebhookEventType.ProgramExecutionFailed,
WebhookEventType.AccuracyThresholdBreached,
WebhookEventType.CanaryRollback
};
public async Task HandleAsync(
WebhookEvent webhookEvent,
WebhookHandlerContext context,
CancellationToken cancellationToken = default)
{
if (context.IsDryRun)
return;
var slackWebhookUrl = context.Configuration["SlackWebhookUrl"] as string;
var message = FormatSlackMessage(webhookEvent);
await SendToSlackAsync(slackWebhookUrl, message, cancellationToken);
}
public bool ValidateConfiguration(IReadOnlyDictionary<string, object> configuration)
{
return configuration.ContainsKey("SlackWebhookUrl") &&
!string.IsNullOrWhiteSpace(configuration["SlackWebhookUrl"] as string);
}
private string FormatSlackMessage(WebhookEvent webhookEvent)
{
// Format message for Slack
return $"Event: {webhookEvent.EventType} at {webhookEvent.Timestamp}";
}
private async Task SendToSlackAsync(string webhookUrl, string message, CancellationToken ct)
{
// Send to Slack API
}
}using Microsoft.Extensions.DependencyInjection;
using Loopai.Core.Plugins;
// In Program.cs or Startup.cs
builder.Services.AddSingleton<IPluginRegistry, PluginRegistry>();
var registry = app.Services.GetRequiredService<IPluginRegistry>();
// Register validators
registry.Register<IValidatorPlugin>(new JsonSchemaValidatorPlugin());
registry.Register<IValidatorPlugin>(new CustomBusinessRuleValidator());
// Register samplers
registry.Register<ISamplerPlugin>(new PercentageSamplerPlugin(0.1));
registry.Register<ISamplerPlugin>(new TimeBasedSamplerPlugin(TimeSpan.FromMinutes(5)));
registry.Register<ISamplerPlugin>(new ErrorRateSamplerPlugin(0.05));
// Register webhook handlers
registry.Register<IWebhookHandlerPlugin>(new SlackWebhookHandlerPlugin());
registry.Register<IWebhookHandlerPlugin>(new ConsoleWebhookHandlerPlugin());appsettings.json:
{
"Loopai": {
"Plugins": {
"Validators": [
{
"Name": "json-schema-validator",
"Type": "validator",
"Enabled": true,
"Priority": 100,
"Configuration": {}
},
{
"Name": "custom-business-rule-validator",
"Type": "validator",
"Enabled": true,
"Priority": 50,
"Configuration": {
"StrictMode": true
}
}
],
"Samplers": [
{
"Name": "percentage-sampler",
"Type": "sampler",
"Enabled": true,
"Priority": 100,
"Configuration": {
"SamplingRate": 0.1
}
}
],
"WebhookHandlers": [
{
"Name": "slack-webhook-handler",
"Type": "webhook-handler",
"Enabled": true,
"Priority": 100,
"Configuration": {
"SlackWebhookUrl": "https://hooks.slack.com/services/..."
}
}
]
}
}
}Code:
builder.Services.Configure<PluginsConfiguration>(
builder.Configuration.GetSection("Loopai:Plugins")
);Plugins are registered with the IPluginRegistry at application startup.
Services query the registry to discover available plugins by type.
Plugins are executed in priority order (higher priority = earlier execution).
Only enabled plugins are executed (check IsEnabled property).
Loopai provides several built-in plugins in Loopai.Core.Plugins.BuiltIn:
- JsonSchemaValidatorPlugin - Validates against JSON schema and expected output
- PercentageSamplerPlugin - Samples a fixed percentage of executions
- TimeBasedSamplerPlugin - Samples based on time intervals
- ConsoleWebhookHandlerPlugin - Logs events to console (development/testing)
- Use descriptive, kebab-case names:
custom-validator,error-rate-sampler - Include organization prefix for clarity:
acme-business-validator
- Use semantic versioning:
1.0.0,1.2.3 - Increment major version for breaking changes
- Always handle exceptions gracefully
- Return meaningful error messages
- Log errors with sufficient context
public async Task<ValidationResult> ValidateAsync(...)
{
try
{
// Validation logic
}
catch (Exception ex)
{
_logger.LogError(ex, "Validation failed for {PluginName}", Name);
return new ValidationResult
{
IsValid = false,
ValidatorType = Name,
Message = $"Validation error: {ex.Message}"
};
}
}- Always validate configuration in
ValidateConfiguration()for webhook handlers - Throw clear exceptions for invalid configuration during plugin construction
- Keep validation logic fast (<100ms recommended)
- Use async operations for I/O-bound work
- Cache expensive computations when possible
- Write unit tests for each plugin
- Test with various input scenarios
- Mock external dependencies
Execute multiple plugins in sequence:
var validators = registry.List<IValidatorPlugin>();
var results = new List<ValidationResult>();
foreach (var validator in validators)
{
var result = await validator.ValidateAsync(execution, context, ct);
results.Add(result);
// Stop on first failure if needed
if (!result.IsValid && stopOnFirstFailure)
break;
}Plugins execute in priority order:
// Higher priority executes first
var criticalValidator = new CriticalValidator { Priority = 1000 };
var standardValidator = new StandardValidator { Priority = 100 };
registry.Register<IValidatorPlugin>(criticalValidator);
registry.Register<IValidatorPlugin>(standardValidator);
// Executes: criticalValidator → standardValidatorEnable/disable plugins at runtime:
var plugin = registry.Resolve<IValidatorPlugin>("my-validator");
if (plugin != null)
{
plugin.IsEnabled = shouldEnable;
}<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<PackageId>YourCompany.Loopai.Plugins</PackageId>
<Version>1.0.0</Version>
<Authors>Your Name</Authors>
<Description>Custom plugins for Loopai</Description>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Loopai.Core" Version="0.1.0" />
</ItemGroup>
</Project>dotnet pack -c Releasedotnet add package YourCompany.Loopai.Pluginsusing Loopai.Core.Plugins;
using Loopai.Core.Models;
using Microsoft.Extensions.Logging;
namespace MyCompany.Loopai.Plugins;
public class AnomalyDetectionSamplerPlugin : ISamplerPlugin
{
private readonly ILogger<AnomalyDetectionSamplerPlugin> _logger;
private readonly double _deviationThreshold;
public AnomalyDetectionSamplerPlugin(
ILogger<AnomalyDetectionSamplerPlugin> logger,
double deviationThreshold = 2.0)
{
_logger = logger;
_deviationThreshold = deviationThreshold;
}
public string Name => "anomaly-detection-sampler";
public string Description => "Samples executions with anomalous latency patterns";
public string Version => "1.0.0";
public string Author => "MyCompany";
public bool IsEnabled { get; set; } = true;
public int Priority { get; set; } = 75;
public Task<SamplingDecision> ShouldSampleAsync(
ExecutionRecord execution,
SamplingContext context,
CancellationToken cancellationToken = default)
{
try
{
// Calculate mean and std deviation from context
var meanLatency = /* calculate from historical data */;
var stdDeviation = /* calculate from historical data */;
// Check if current execution is anomalous
var deviation = Math.Abs(execution.LatencyMs - meanLatency) / stdDeviation;
var isAnomalous = deviation > _deviationThreshold;
if (isAnomalous)
{
_logger.LogWarning(
"Anomalous latency detected: {Latency}ms (mean: {Mean}ms, deviation: {Deviation}σ)",
execution.LatencyMs,
meanLatency,
deviation
);
}
return Task.FromResult(new SamplingDecision
{
ShouldSample = isAnomalous,
Reason = isAnomalous
? $"Anomalous latency: {deviation:F2}σ from mean"
: "Normal latency pattern",
SamplerType = Name,
ConfidenceScore = isAnomalous ? deviation / 10.0 : 0.0,
Metadata = new Dictionary<string, object>
{
["mean_latency_ms"] = meanLatency,
["std_deviation_ms"] = stdDeviation,
["deviation_sigma"] = deviation,
["threshold_sigma"] = _deviationThreshold
}
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in anomaly detection sampler");
return Task.FromResult(new SamplingDecision
{
ShouldSample = false,
Reason = $"Error: {ex.Message}",
SamplerType = Name
});
}
}
}- Documentation: Loopai Docs
- Issues: GitHub Issues
- Examples: See
src/Loopai.Core/Plugins/BuiltIn/for reference implementations
Plugin development follows the same MIT license as Loopai core.