Edge Cases
Null paths, cycle detection, depth limits, isolation violations, and other boundary conditions.
1. No Wildcard in Field Value
If the field string contains no wildcard syntax, it short-circuits immediately at the orchestrator level — no parsing, no directive call.
// Input
"hello world"
// Result: EvaluationResponse.ShortCircuit("hello world")
// IsSuccess = true, Value = "hello world", FormattedValue = "hello world"
// DurationMs ≈ 0
2. Path Not Found / Null Value
When a dot-path does not exist in the data source:
// {@ $var.missingVariable }
// Memory does not contain "missingVariable"
// Without @default: → Failure
// ErrorCode = PathNotFound, ErrorMessage = "Variable 'missingVariable' not found"
// With @default:0 → Success
// {@ $var.missingVariable @default:0 }
// Value = "0"
$ctx.env.MISSING returns PathNotFound. $input.current.missingField may return null or PathNotFound depending on the item structure. Always use @default when a path may legitimately be absent.
3. Depth Limit Exceeded (Depth ≥ 10)
Every time a directive chains to another directive or expression, Depth increments. At depth = 10, the chain is hard-stopped.
// Scenario: @aliasA → {@ @aliasB } → {@ @aliasC } → ... → depth 10
// At depth 10: GuardChainDepth() throws → converted to:
// EvaluationResponse.Failure(EvaluationErrorCode.DepthLimitExceeded, ...)
GuardChainDepth() before invoking ChainExpressionAsync or ChainToDirectiveAsync. Failure to do so can allow runaway recursion until a stack overflow.
4. Cycle Detection via VisitedKeys
The VisitedKeys set on the EvaluationRequest tracks which expression keys have already been evaluated in this chain. If a key appears twice, it is a cycle.
// Scenario: Canned expression @loop contains "{@ @loop }"
// First evaluation: VisitedKeys = {"loop"}
// ChainExpressionAsync("{@ @loop }") → adds "loop" → already in set
// → EvaluationResponse.Failure(EvaluationErrorCode.CycleDetected)
request.Chain() and request.ForSubDirective() return a NEW request with the key added to an immutable copy of VisitedKeys. The parent request's set is never mutated.
5. Isolation Level Violation
Using $js, $cs, or $api when the context's IsolationLevel is too low produces a hard failure — not a silent null.
// Context.IsolationLevel = Safe
// Expression: {@ $js`return 42` }
// → Failure(IsolationViolation,
// "$js requires Sandboxed isolation level or higher")
The node executor or workflow engine is responsible for setting the correct isolation level in EvaluationContext based on the node type's declared requirements.
6. Unknown Directive Name
If the parser extracts a directive name that is not registered in DI:
- Orchestrator checks for
IDefaultExpressionDirectiveService - If a default is registered → routes there
- If no default →
Failure(DirectiveNotFound)
// No $weather directive registered
// {@ $weather.temperature }
// → routes to default directive (e.g., $var)
// → tries to resolve "weather.temperature" as a memory variable
// → likely Failure(PathNotFound)
7. Malformed Expression Syntax
// Unclosed brace
"{@ $ctx.user.name" // no closing }
// → CanParse = false → treated as plain string (no evaluation)
// Empty directive
"{@ }"
// → ParseError
// Missing dollar sign (AtBrace parser only)
"{@ ctx.user.name }"
// → ParseError (no $-prefixed directive token found)
8. Template with a Failing Sub-expression
In a template string, if one region fails, the orchestrator does NOT fail the entire template. The failed region is substituted with an empty string (or the error message in trace mode).
// Template: "Hello {@ $ctx.user.name }, order {@ $var.missingId } done."
// $ctx.user.name → "Alice"
// $var.missingId → Failure(PathNotFound)
// Result: "Hello Alice, order done."
// (missing ID substituted with empty string)
// EnableExpressionTrace=true → "Hello Alice, order [PathNotFound:missingId] done."
9. Binary Field Access
Binary fields (file attachments, images) in node input have a special sub-path prefix:
// Accessing binary data
"{@ $input.current._binary.attachment }"
// Returns binary descriptor (filename, mimeType, data reference)
// NOT the raw bytes inline — use @base64 option to encode
"{@ $input.current._binary.profileImage @base64 }"
// Returns Base64-encoded image data
10. Concurrent Node Execution
EvaluationContext should NOT be shared across concurrent node executions. Each parallel branch in a workflow must get its own context (different NodeKey, different IExpressionCache scope). The context's cache is not thread-safe across different NodeKey values.
// ✅ Correct: each parallel node gets its own context
var ctx1 = BuildContext(node1, executionState);
var ctx2 = BuildContext(node2, executionState);
// ❌ Wrong: sharing context across parallel nodes
var sharedCtx = BuildContext(...);
await Task.WhenAll(
orchestrator.EvaluateAsync(field1, sharedCtx, ct), // race condition on cache
orchestrator.EvaluateAsync(field2, sharedCtx, ct)
);
11. Cache Miss After Scope Invalidation
Cache entries at Node scope are invalidated when a node finishes. If a directive accidentally stores results in a wider scope (Thread or Process) without being told to, stale data may persist.
ICacheMarkerOption in the expression. Directives only READ from the cache.
12. $api Domain Not Allowlisted
// {@ $api.MyService/data.field }
// "MyService" resolves to https://api.untrusted-external.com
// Not in ExpressionEvaluation:Api:AllowedDomains
// → Failure(DomainNotAllowed,
// "Domain 'api.untrusted-external.com' is not in the allowed list")
Error Code Quick Reference
| Situation | ErrorCode | Recoverable? |
|---|---|---|
| Path doesn't exist in data | PathNotFound | Yes — use @default |
| Directive name not registered | DirectiveNotFound | No — register the directive |
| Chain depth ≥ 10 | DepthLimitExceeded | No — fix circular expressions |
| Same key twice in chain | CycleDetected | No — fix circular aliases |
| Wrong isolation level | IsolationViolation | Yes — raise isolation level |
| Script throws | ScriptError | Yes — fix script logic |
| Required field is null | ValidationError | Yes — ensure data is present |
| HTTP call failed | ExternalApiError | Yes — retry or fix endpoint |
| Target domain blocked | DomainNotAllowed | Yes — add to allowlist |
| Template render error | TemplateRenderError | Yes — fix Liquid syntax |
| Alias not in DB | CannedExpressionNotFound | Yes — create the alias |
| Directive timed out | Timeout | Yes — optimize or increase limit |