Portal Community
PropertyValue
Directive name@ (AtSign prefix)
Required isolation levelSafe (inherited from stored expression)
ProjectCatalog.Services
Data sourceExpression_CannedExpressions table via ICannedExpressionStore
Syntax form{@ @AliasName } or bare @AliasName in supporting contexts

Syntax

Canned expression by alias
{@ @TenantCurrency }
Stored body: {@ $ctx.tenant.currency } → evaluated to "EUR"
Embedded in a template string
Invoice {@ $input.current.invoiceNumber } — Total: {@ @FormattedTotal }
FormattedTotal expands to its stored expression and is evaluated inline
With options
{@ @EmployeeFullName @uppercase }
Canned expression resolved, then result uppercased

Resolution Order

When resolving @AliasName, the system searches in order and uses the first match:

  1. TenantID = current tenant, AppID = current app, Name = alias
  2. TenantID = current tenant, AppID = NULL (tenant-wide), Name = alias
  3. TenantID = NULL (platform-wide), AppID = NULL, Name = alias

This allows platform defaults to be overridden per-tenant or per-app without changing workflow definitions.

Database Schema

CREATE TABLE Expression_CannedExpressions (
    ID            INT IDENTITY,
    TenantID      INT NULL,          -- NULL = platform-wide default
    AppDomainID   INT NULL,          -- NULL = tenant-wide (all apps)
    Name          NVARCHAR(200) NOT NULL,   -- the alias (e.g. "TenantCurrency")
    Body          NVARCHAR(MAX) NOT NULL,   -- expression text (may contain wildcards)
    Description   NVARCHAR(500) NULL,
    IsActive      BIT DEFAULT 1,
    CreatedOn     DATETIME NOT NULL,
    CreatedBy     INT NOT NULL,
    LastModifiedOn DATETIME NULL,
    LastModifiedBy INT NULL,
    -- ... full standard audit columns
    CONSTRAINT UQ_CannedExpr UNIQUE (TenantID, AppDomainID, Name)
)

Multi-Pass Resolution

Canned expressions are the primary driver of multi-pass resolution. When a canned body itself contains BizFirst wildcards, those wildcards are resolved in subsequent passes:

Pass 1:  {@ @GreetingLine }
         ↓ loads body: "Dear {@ $ctx.user.firstName }, welcome to {@ $ctx.tenant.name }"
Pass 2:  wildcards inside the body are evaluated
         ↓ result: "Dear Alice, welcome to Acme Corp"
VisitedKeys prevents infinite loops If canned expression A expands to body that references @A again, the VisitedKeys set catches the cycle and raises CyclicReference rather than looping infinitely.

Complex Scenarios

Scenario 1 — Tenant-Specific Legal Disclaimer

Each tenant has a different legal footer. A single workflow uses one canned alias — each tenant's record returns their own text:

// Platform default (TenantID = NULL):
@LegalDisclaimer → "This document is generated automatically. For queries contact support@bizfirst.ai."

// Acme Corp override (TenantID = 5):
@LegalDisclaimer → "Acme Corp. Registered in Ireland. Company No. 123456. VAT IE9876543A."

// Usage in email template — same workflow, different output per tenant:
{@ @LegalDisclaimer }

Scenario 2 — Composed Expression from Multiple Canned Parts

Build a reusable email greeting from several canned pieces, each independently overridable:

// @GreetingLine body:    "Dear {@ $ctx.user.firstName },"
// @SignatureLine body:   "{@ $ctx.user.name }\n{@ $ctx.user.department } Manager"
// @TenantSupport body:  "{@ $ctx.tenant.name } Support — {@ $ctx.env.SUPPORT_EMAIL }"

// Email node:
{@ @GreetingLine }

Your payslip for {@ $ctx.now @date:MMMM yyyy } is attached.

Kind regards,
{@ @SignatureLine }

---
{@ @TenantSupport }

Scenario 3 — Shared Validation Rule

A canned expression encapsulates a reusable business rule used across multiple workflows:

// @IsHighValueTransaction body:
//   "{@ $js`return parseFloat(context.input.current.amount) > 50000` }"

// Used in multiple workflows — change the threshold in ONE place:
{@ @IsHighValueTransaction }
// IfCondition: true → route to finance approval; false → auto-approve

// When threshold changes from 50000 to 75000:
// Update ONE DB record; all workflows using @IsHighValueTransaction update instantly.

Scenario 4 — Nested Canned Expressions

A canned expression that references another canned expression — resolved recursively via multi-pass:

// @AuditHeader body:
//   "[{@ $ctx.now }] Tenant: {@  @TenantRef } | Exec: {@ $exec.executionId }"

// @TenantRef body:
//   "{@ $ctx.tenant.id }:{@ $ctx.tenant.name }"

// Usage:
{@ @AuditHeader }

// Pass 1: expands @AuditHeader
// Pass 2: expands $ctx.now, @TenantRef, $exec.executionId
// Pass 3: expands $ctx.tenant.id, $ctx.tenant.name (from @TenantRef body)
// Result: "[2024-03-15T10:22:01Z] Tenant: 5:Acme Corp | Exec: e8f23a1b-..."

Scenario 5 — App-Specific Override for Multi-App Tenant

A tenant has two apps (Payroll UK, Payroll IE). The same alias resolves differently per app:

// @PayPeriodLabel entries:
// TenantID=5, AppDomainID=12 (Payroll UK): "Tax Week"
// TenantID=5, AppDomainID=15 (Payroll IE): "Pay Period"
// TenantID=NULL, AppDomainID=NULL (default): "Period"

// Same workflow runs in both apps:
{@ @PayPeriodLabel }: {@ $input.current.periodStart }{@ $input.current.periodEnd }
// UK app → "Tax Week: 2024-04-06 – 2024-04-12"
// IE app → "Pay Period: 2024-04-01 – 2024-04-30"

Scenario 6 — REST API Management of Canned Expressions

Canned expressions are managed via REST API — enabling admin UIs and CI/CD pipeline updates:

// List all tenant-scoped canned expressions
GET /api/expressions/canned?tenantId=5

// Create a new one
POST /api/expressions/canned
{
  "tenantId": 5,
  "appDomainId": null,
  "name": "SupportEmail",
  "body": "{@ $ctx.env.SUPPORT_EMAIL @default:support@bizfirst.ai }",
  "description": "Tenant support email address, falls back to platform default"
}

// Preview resolution without executing a workflow
POST /api/expressions/canned/preview
{
  "name": "SupportEmail",
  "tenantId": 5
}
// Returns: { "resolved": "helpdesk@acme.com", "passCount": 2 }

Common Errors

ErrorCauseFix
PathNotFound: @MyAliasNo matching record in Expression_CannedExpressions for this tenant/app/nameCreate the entry via admin UI or REST API; check spelling (case-sensitive)
CyclicReference: @MyAliasCanned expression body references itself directly or indirectlyBreak the cycle by creating intermediate variables; VisitedKeys detects the loop
DepthLimitExceededDeep chain of nested canned expressions exceeds 10 passesFlatten nested canned expressions; increase depth limit with caution
Wrong tenant's value returnedResolution order matched a broader-scoped record unexpectedlyAdd a more specific record (TenantID + AppDomainID) to override the fallback