Flow Studio
Building a Custom Engine
A custom expression engine implements IExpressionEvaluator and is registered in DI alongside (or instead of) the default Jint engine. Custom engines are useful for supporting alternative expression languages such as CEL, FEEL, JSONPath, or XPath.
Minimal Custom Evaluator
// CelExpressionEvaluator.cs
using BizFirst.Ai.ProcessEngine.JS.Services.ExpressionEngine;
using BizFirst.Ai.ProcessEngine.Domain.Definition;
/// CEL (Common Expression Language) evaluator implementation.
public class CelExpressionEvaluator : IExpressionEvaluator
{
private readonly ICelEngine _celEngine;
private readonly ILogger<CelExpressionEvaluator> _logger;
public CelExpressionEvaluator(ICelEngine celEngine,
ILogger<CelExpressionEvaluator> logger)
{
_celEngine = celEngine;
_logger = logger;
}
public async Task<object?> EvaluateAsync(
string expression,
Dictionary<string, object> executionVariables,
ProcessElementDefinition? elementDefinition = null)
{
if (string.IsNullOrWhiteSpace(expression))
throw new ArgumentException("Expression cannot be empty", nameof(expression));
try
{
// Evaluate CEL expression with the provided variables
return await _celEngine.EvaluateAsync(expression, executionVariables);
}
catch (Exception ex)
{
_logger.LogError(ex, "CEL evaluation failed for expression: {Expression}", expression);
throw new InvalidOperationException($"CEL evaluation error: {ex.Message}", ex);
}
}
public async Task<bool> EvaluateBooleanAsync(
string expression,
Dictionary<string, object> executionVariables,
ProcessElementDefinition? elementDefinition = null)
{
var result = await EvaluateAsync(expression, executionVariables, elementDefinition);
return result is bool b ? b : result is not null;
}
public Task<ExpressionValidationResult> ValidateExpressionAsync(string expression)
{
try
{
_celEngine.ValidateSyntax(expression);
return Task.FromResult(ExpressionValidationResult.Valid());
}
catch (Exception ex)
{
return Task.FromResult(
ExpressionValidationResult.Invalid($"CEL syntax error: {ex.Message}"));
}
}
}
Registering a Custom Evaluator
Register the custom evaluator in DI. To replace the default Jint engine, register it as the primary IExpressionEvaluator. To add it alongside (for specific node types), register it as a named service:
// In your DI registration / INodeExecutorDependency.RegisterDefaults()
// Option A: Replace the default evaluator entirely
services.AddScoped<ICelEngine, GoogleCelEngine>();
services.AddScoped<IExpressionEvaluator, CelExpressionEvaluator>();
// Note: this REPLACES the Jint evaluator for all expression resolution
// Option B: Named registration (for use by specific executors only)
services.AddKeyedScoped<IExpressionEvaluator, CelExpressionEvaluator>("cel");
// Resolve with: serviceProvider.GetKeyedService<IExpressionEvaluator>("cel")
Expression Prefix Routing
If running multiple evaluators side-by-side, use a prefix convention in the expression string to route to the correct engine. The ConfigExpressionResolver can detect the prefix and dispatch accordingly:
// Prefix-based routing in the expression field value
@{js: $output['node-1'].statusCode === 200} // → Jint engine
@{cel: output['node-1'].statusCode == 200} // → CEL engine
@{feel: output.node-1.statusCode = 200} // → FEEL engine
Security Responsibility
When implementing a custom evaluator, you are responsible for sandboxing and timeout enforcement. The Jint evaluator uses
JintSecurityConfiguration.CreateEngine(isolationMode) to enforce resource limits. Your custom engine must apply equivalent constraints — unconstrained evaluation creates a denial-of-service risk.
Validation Is Critical
Always implement
ValidateExpressionAsync — it is called when the user saves a workflow in the designer. Without validation, syntax errors are only caught at execution time, which is a poor developer experience.