Portal Community

Contents

  1. Single Expression vs Template String
  2. Parallel Interpolation
  3. Multi-Pass Resolution
  4. VisitedKeys — Preventing Cycles
  5. Depth Tracking Across Passes
  6. $tpl — Liquid Template Directive
  7. Sample Walkthroughs

Single Expression vs Template String

The parser classifies a field value into one of three cases:

CaseExampleHandling
No wildcard "hello world" Short-circuit — returned as-is, no evaluation
Single expression "{@ $var.name }" Parse once → single directive invocation → return value
Template string "Hello {@ $ctx.user.name }, order {@ $var.id } is ready" FindAllRegions → parallel evaluation → string assembly → multi-pass if needed

IWildcardParser.IsTemplate(text) returns true when: (a) there are two or more expressions, or (b) there is literal text outside the expression boundary.

Parallel Interpolation

When a template string is detected, all regions are evaluated simultaneously:

// Field value:
"Dear {@ $ctx.user.name }, your balance is {@ $var.balance @json } as of {@ $ctx.today }"

// Step 1: FindAllRegions() → 3 regions
// Step 2: Task.WhenAll — all 3 evaluated concurrently
//   Region 0: $ctx.user.name  → "Alice"
//   Region 1: $var.balance    → 1250.50
//   Region 2: $ctx.today      → "2024-03-15"

// Step 3: Substitute FormattedValues
// Result: "Dear Alice, your balance is 1250.5 as of 2024-03-15"
Parallel, not sequential Regions are evaluated with Task.WhenAll. If your expressions are independently resolvable (most are), this is free parallelism. The order of substitution is determined by region positions, not evaluation order.

Multi-Pass Resolution

After assembling the interpolated string, the orchestrator scans it again for wildcards. If new wildcards are found (because a resolved value itself contained an expression), it re-enters the pipeline:

// Pass 1:
"Recipient: {@ @report.recipient }"
// @report.recipient → "{@ $ctx.tenant.billingEmail }"  (this alias IS an expression)
// Assembled: "Recipient: {@ $ctx.tenant.billingEmail }"

// Scan → new wildcards found → Pass 2:
"Recipient: {@ $ctx.tenant.billingEmail }"
// $ctx.tenant.billingEmail → "billing@acme.com"
// Assembled: "Recipient: billing@acme.com"

// Scan → no wildcards → done
// Final result: "Recipient: billing@acme.com"

Multi-pass continues as long as new wildcards are found AND Depth < 10. Each pass increments depth by 1.

VisitedKeys — Preventing Cycles

The VisitedKeys set is shared across all passes. Each expression's unique key is added before evaluation. If it's already in the set, a cycle is detected:

// Scenario: canned expression loop
// @loopA → "{@ @loopB }"
// @loopB → "{@ @loopA }"

// Pass 1: evaluate @loopA → key "loopA" added to VisitedKeys
// Result: "{@ @loopB }"
// Pass 2: evaluate @loopB → key "loopB" added
// Result: "{@ @loopA }"
// Pass 3: @loopA → "loopA" already in VisitedKeys → CycleDetected error
VisitedKeys is carried forward, not reset between passes This is intentional. It catches cycles that span multiple passes — e.g., A → B → A — which Depth alone cannot catch if each hop is shallow.

Depth Tracking Across Passes

Both multi-pass interpolation and directive chaining share the same Depth counter on the EvaluationRequest:

This prevents the combination of chaining + multi-pass from bypassing the depth limit:

// 5 levels of canned expression chaining (depth 0→5)
// + 5 multi-pass iterations (depth 5→10)
// = hit depth limit at exactly 10

$tpl — Liquid Template Directive

The $tpl directive is a specialized case of template rendering. It uses the Fluid (Liquid) library and handles its own internal variable substitution — it does NOT re-enter the BizFirst expression pipeline for Liquid {{ variable }} references. Instead, it populates the Liquid context from EvaluationContext:

Liquid variableSource
{{ user.name }}Context.CurrentUser.Name
{{ tenant.name }}Context.TenantId → metadata provider
{{ tenant.timezone }}Tenant metadata
{{ exec.executionId }}Context.ExecutionId
{{ input.fieldName }}Context.NodeExecutionData.CurrentItem
{{ vars.myVar }}Context.Memory
{{ now }}DateTime.UtcNow
$tpl is a separate rendering system BizFirst expressions ({@ $var.x }) and Liquid expressions ({{ x }}) are different syntaxes processed by different engines. A $tpl directive renders Liquid — the result is a plain string. If that string needs further BizFirst expression evaluation, it would require another pass through the orchestrator.

Sample Walkthroughs

Sample 1 — Email Subject with Dynamic Values

// Field: Email Subject
"[{@ $ctx.tenant.name }] Invoice #{@ $var.invoiceNumber } — {@ $var.amount @default:0 } {@ @company.currency }"

// Regions found: 4
// Parallel evaluation:
//   $ctx.tenant.name   → "Acme Corp"
//   $var.invoiceNumber → "INV-2024-001"
//   $var.amount        → 4500
//   @company.currency  → "EUR" (via canned expression → $ctx.tenant.currency)
// Assembled: "[Acme Corp] Invoice #INV-2024-001 — 4500 EUR"
// Scan → no new wildcards → done (1 pass)

Sample 2 — Nested Canned Expression (2 passes)

// Canned: @hr.approver → "{@ @tenant.hrManager }"
// Canned: @tenant.hrManager → "{@ $ctx.tenant.hrManagerEmail }"

// Field: "Approval required from: @hr.approver"

// Pass 1: @hr.approver → "{@ @tenant.hrManager }"
// Assembled: "Approval required from: {@ @tenant.hrManager }"
// Scan → wildcard found → Pass 2

// Pass 2: @tenant.hrManager → "{@ $ctx.tenant.hrManagerEmail }"
// Assembled: "Approval required from: {@ $ctx.tenant.hrManagerEmail }"
// Scan → wildcard found → Pass 3

// Pass 3: $ctx.tenant.hrManagerEmail → "hr@acme.com"
// Assembled: "Approval required from: hr@acme.com"
// Scan → no wildcards → done (3 passes, depth 3)

Sample 3 — $tpl with Context Data

// Node field: "{@ $tpl.InvoiceEmail }"

// $tpl directive:
// 1. Loads template "InvoiceEmail" from database
// 2. Builds Liquid context from EvaluationContext
// 3. Renders Liquid → plain HTML string
// 4. Returns EvaluationResponse.Success(htmlString)

// Liquid template stored in DB:
// ---
// Dear {{ user.name }},
//
// Your invoice {{ vars.invoiceNumber }} for {{ vars.amount | money }} is ready.
// Payment due: {{ vars.dueDate | date: "%B %d, %Y" }}
// ---

// Result: fully rendered HTML email body (single BizFirst evaluation pass)