Portal Community

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:

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());
Multiple Registrations You can register multiple 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:

Use Case: Access configuration values set at workflow start without typing the full path.
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:

Use Case: Quick access to current user properties without referencing specific input nodes.
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:

Use Case: Standardized access to API responses with automatic 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 CasePrefixExpansionExample
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}
Registration Timing Shorthands must be registered before the first expression is evaluated. Add them in Startup.cs or Program.cs during service configuration, not dynamically at runtime.
Avoid Circular Expansions Do not create shorthands that expand to other shorthands. The resolver may not handle this correctly:
// 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}}}";