Custom Shorthands
The built-in shorthands cover the most common patterns, but you can define your own. Create custom shorthands by implementing IShorthandDefinition and registering them in the dependency injection container. This guide walks through the process with real examples.
Overview
A custom shorthand maps a prefix to an expansion pattern. When the expression resolver encounters your prefix, it expands the full expression before evaluation. This allows you to:
- Create domain-specific aliases (e.g.,
$userfor user data) - Combine commonly-used modifiers (e.g.,
$configwith automatic JSON parsing) - Enforce team standards for data access patterns
- Abstract implementation details from workflow designers
The ShorthandDefinition Interface
Custom shorthands implement IShorthandDefinition:
public interface IShorthandDefinition
{
/// <summary>
/// The prefix that triggers this shorthand.
/// Example: "$config" or "@@api"
/// </summary>
string Prefix { get; }
/// <summary>
/// Expand the shorthand into a full directive.
/// Input: "myKey" (the part after the prefix)
/// Output: The full @{...} directive to execute
/// </summary>
string Expand(string shorthandPath);
}
Step-by-Step Implementation Guide
Step 1: Create a Custom Shorthand Class
Implement IShorthandDefinition to define your custom prefix and expansion logic:
using BizFirst.Ai.ProcessEngine.JS.Directives;
/// <summary>
/// Custom shorthand: $config
/// Expands: @{$config:settingName}
/// Into: @{memory:config.settingName}
/// Purpose: Access configuration from execution memory with standard prefix
/// </summary>
public class ConfigShorthandDefinition : IShorthandDefinition
{
public string Prefix => "$config";
public string Expand(string shorthandPath)
{
// shorthandPath = "settingName"
// Return full directive that references memory
return $"@{{memory:config.{shorthandPath}}}";
}
}
Step 2: Register in the DI Container
Add your shorthand to the service collection during application startup:
// In Startup.cs or Program.cs
services.AddSingleton<IShorthandDefinition>(new ConfigShorthandDefinition());
services.AddSingleton<IShorthandDefinition>(new UserShorthandDefinition());
services.AddSingleton<IShorthandDefinition>(new ApiShorthandDefinition());
IShorthandDefinition implementations. The resolver collects all registered shorthands and uses them during expression evaluation.
Real Example 1: $config Shorthand
A shorthand for accessing application configuration stored in execution memory:
public class ConfigShorthandDefinition : IShorthandDefinition
{
public string Prefix => "$config";
public string Expand(string shorthandPath)
{
// Input: maxRetries
// Output: @{memory:config.maxRetries}
return $"@{{memory:config.{shorthandPath}}}";
}
}
// Usage:
// Workflow designer writes: @{$config:maxRetries}
// System expands to: @{memory:config.maxRetries}
// Resolves to: 3 (the actual value stored)
In Action:
// Designer writes:
Max Retry Count: @{$config:maxRetries}
// System resolves:
Max Retry Count: 3
// vs. without shorthand (would require):
Max Retry Count: @{memory:config.maxRetries}
Real Example 2: $user Shorthand
A shorthand for accessing user data passed through execution context:
public class UserShorthandDefinition : IShorthandDefinition
{
public string Prefix => "$user";
public string Expand(string shorthandPath)
{
// Input: firstName
// Output: @{context:currentUser.firstName}
// OR: @{memory:user.firstName}
// depending on your architecture
return $"@{{memory:currentUser.{shorthandPath}}}";
}
}
// Usage:
// Workflow designer writes: @{$user:firstName}
// System expands to: @{memory:currentUser.firstName}
// Resolves to: "Alice"
In Action:
// Designer writes:
Display Name: @{js: @{$user:firstName} + ' ' + @{$user:lastName}}
// System expands to:
Display Name: @{js: @{memory:currentUser.firstName} + ' ' + @{memory:currentUser.lastName}}
// Resolves to:
Display Name: Alice Johnson
Real Example 3: @api Shorthand
A shorthand for API endpoints that includes JSON parsing:
public class ApiShorthandDefinition : IShorthandDefinition
{
public string Prefix => "@api";
public string Expand(string shorthandPath)
{
// Input: webhook-1.body
// Output: @{output:webhook-1.body | parseJson}
// Automatically parses JSON in one shorthand
return $"@{{output:{shorthandPath} | parseJson}}";
}
}
// Usage:
// Workflow designer writes: @{@api:webhook-1.body}
// System expands to: @{output:webhook-1.body | parseJson}
// Resolves to: { "id": 123, "name": "Alice" }
In Action:
// Designer writes:
API Result: @{@api:rest-endpoint-1.response}
// System expands to:
API Result: @{output:rest-endpoint-1.response | parseJson}
// Resolves to (parsed object):
API Result: { "status": "success", "data": [...] }
// vs. without shorthand (would require):
API Result: @{output:rest-endpoint-1.response | parseJson}
Advanced: Multiple Prefixes
You can create shorthand classes that support multiple related prefixes:
public class DataSourceShorthandDefinition : IShorthandDefinition
{
private readonly string _prefix;
// Constructor allows reuse with different prefixes
public DataSourceShorthandDefinition(string prefix)
{
_prefix = prefix;
}
public string Prefix => _prefix;
public string Expand(string shorthandPath)
{
return _prefix switch
{
"$db" => $"@{{memory:database.{shorthandPath}}}",
"$cache" => $"@{{memory:cache.{shorthandPath}}}",
"$local" => $"@{{memory:localStorage.{shorthandPath}}}",
_ => throw new InvalidOperationException($"Unknown prefix: {_prefix}")
};
}
}
// Register multiple variants
services.AddSingleton<IShorthandDefinition>(new DataSourceShorthandDefinition("$db"));
services.AddSingleton<IShorthandDefinition>(new DataSourceShorthandDefinition("$cache"));
services.AddSingleton<IShorthandDefinition>(new DataSourceShorthandDefinition("$local"));
Testing Custom Shorthands
Test your shorthand by verifying the expansion and resolution:
// Unit test example
[Test]
public void ConfigShorthand_ExpandsCorrectly()
{
// Arrange
var shorthand = new ConfigShorthandDefinition();
// Act
string expanded = shorthand.Expand("maxRetries");
// Assert
Assert.AreEqual("@{memory:config.maxRetries}", expanded);
}
[Test]
public void UserShorthand_ResolvesInExpression()
{
// Arrange
var shorthand = new UserShorthandDefinition();
var context = new EvaluationContext { /* ... */ };
// Act
string expanded = shorthand.Expand("firstName");
var result = evaluator.Evaluate(expanded, context);
// Assert
Assert.AreEqual("Alice", result);
}
Best Practices
1. Use Consistent Prefix Patterns
// Good: Clear prefixes
$config // Single $ for direct references
$user
$api
$$parsed // Double $ for parsed/transformed
@@custom // @ for special types
// Avoid: Ambiguous prefixes
$var_1 // Looks like it might collide with @{var:...}
@temp // Too generic
2. Document the Expansion Pattern
public class MyShorthand : IShorthandDefinition
{
/// <summary>
/// Maps $myprefix:path to @{memory:mydata.path}
/// Example: @{$myprefix:value} → @{memory:mydata.value}
/// </summary>
public string Prefix => "$myprefix";
public string Expand(string shorthandPath) { ... }
}
3. Keep Expansions Simple
// Good: Single clear directive
return $"@{{memory:{shorthandPath}}}";
// Avoid: Complex nested directives
return $"@{{js: parseJson(@{{memory:{shorthandPath}}}) }}";
4. Validate Paths
public string Expand(string shorthandPath)
{
if (string.IsNullOrWhiteSpace(shorthandPath))
throw new ArgumentException("Shorthand path cannot be empty");
// Sanitize path to prevent injection
var safePath = SanitizePath(shorthandPath);
return $"@{{memory:{safePath}}}";
}
5. Plan for Conflicts
// If you must support overlapping prefixes, use longer prefixes
$config // Good
$cfg // Okay but shorter
$c // Bad - too likely to conflict
// Use secondary registration if prefix might conflict
if (!registeredPrefixes.Contains("$data"))
services.AddSingleton<IShorthandDefinition>(new DataShorthand());
Common Patterns
| Use Case | Prefix | Expansion | Example |
|---|---|---|---|
| Application Config | $config |
@{memory:config.KEY} |
@{$config:apiKey} |
| User Data | $user |
@{memory:user.KEY} |
@{$user:email} |
| API Responses | @api |
@{output:NODE | parseJson} |
@{@api:rest-1.response} |
| Database Results | $db |
@{memory:database.KEY} |
@{$db:userId} |
| Cache Data | $cache |
@{memory:cache.KEY} |
@{$cache:sessionId} |
| Feature Flags | $feature |
@{memory:features.KEY} |
@{$feature:betaMode} |
Startup.cs or Program.cs during service configuration, not dynamically at runtime.
// Bad: $quick expands to another shorthand
public string Expand(string path) => $"@{{$config:{path}}}";
// Good: Expand directly to a directive
public string Expand(string path) => $"@{{memory:config.{path}}}";