Skip to main content
Reliant workflows use CEL (Common Expression Language) for dynamic values. CEL is a simple, safe expression language that allows you to reference data, perform calculations, and make decisions within your workflow YAML.

Overview

In Reliant workflows, all string values in YAML are treated as potential CEL expressions. The template syntax {{expression}} is used to embed CEL expressions within strings.

Key Concepts

  1. Template Interpolation: Use {{expression}} to insert dynamic values into strings
  2. Pure Expressions: When an entire value is {{expression}}, the native type is preserved (not converted to string)
  3. Literal Strings: Text without {{}} is treated as a literal value
  4. Explicit Namespaces: All data access uses explicit namespaces (inputs.*, nodes.*, etc.)

Quick Examples


Template Syntax

Basic Interpolation

Use double curly braces to embed expressions within strings:
Multiple expressions can appear in a single string:

Type Preservation

When an entire YAML value is a single {{expression}}, the expression’s native type is preserved:

Literal Values

Text without {{}} is passed through as-is:

Available Namespaces

Reliant uses an explicit namespace model—all data access must use a namespace prefix. There is no implicit variable injection.

inputs.*

Accesses workflow input parameters defined in the workflow schema.
Common patterns:

nodes.*

Accesses outputs from completed workflow nodes. Only available after the referenced node has completed.

Optional Chaining (?.) for Conditional Nodes

When accessing nodes that may have been skipped (due to a condition: on the node), use optional chaining to safely handle the case where the node output doesn’t exist:
Common CallLLM outputs: stop_reason values: Common Run outputs (shell commands): Loop outputs: Loop outputs contain the last iteration’s sub-workflow outputs directly. Access them via nodes.<loop_id>.<output_name> where <output_name> matches outputs declared in the loop’s inline workflow.

workflow.*

Provides workflow metadata and execution context.

iter.*

Provides loop iteration context. Only available inside loop constructs.
Note: Previous iteration data is available via outputs.* in while conditions. For inject templates, use thread: mode: inherit to preserve context across iterations.

output.*

Used in save_message blocks to reference the current activity’s output.
Note: Token counts (input_tokens, output_tokens, etc.) and thinking are automatically extracted from activity output - no explicit configuration needed.

outputs.*

Used in loop while conditions to reference the sub-workflow’s declared outputs.

Built-in Functions

Standard CEL Functions

String Functions

Reliant Custom Functions

first(list)

Returns the first element of a list as an optional. Returns optional.none() if the list is empty. Use .orValue(default) to unwrap with a default value, or .value() to unwrap (errors on empty).

last(list)

Returns the last element of a list as an optional. Returns optional.none() if the list is empty. Use .orValue(default) to unwrap with a default value, or .value() to unwrap (errors on empty).

join(list, delimiter)

Joins list elements into a string with a delimiter.

parseJson(string)

Parses a JSON string into a CEL value (map or list).

toJson(value)

Converts a value to its JSON string representation.

coalesce(value1, value2, ...)

Returns the first non-null argument. Supports 2-4 arguments.

getOrDefault(map, key, default)

Safely accesses a map key with a fallback default value.

spawn(workflowRef, presets)

Generates a tool filter entry for spawning sub-agents. Used within tool_filter CEL expressions.
Returns a filter string like spawn:builtin://agent(general,researcher). If the presets list is empty, returns an empty string (effectively disabling spawn).

Response Tool Data Access

Response tool data is available directly on ExecuteTools output via the response_data field. This field contains parsed response data keyed by tool name. Response tools use JSON Schema to define structured outputs. A common pattern is choice/value:
Access the structured response via response_data:
For more complex schemas, you can define arrays and nested objects:

parseDuration(string)

Parses a Go duration string and returns seconds as a number.

Common Patterns

Conditional Logic

Use the ternary operator for conditional values:

Default Values

Several patterns for handling missing or empty values:

Checking Optional Fields

Use has() to safely check for optional fields:

Working with Tool Calls

Common patterns for handling tool calls from LLM responses:

Loop Iteration Patterns

Using iteration context in loops:

String Interpolation

Embed multiple values in strings:

Type Handling

CEL Type System

CEL is strongly typed. Values have specific types that affect how operators and functions work.

Null Checks

Always check for null before accessing nested fields:

Type Coercion

CEL performs limited automatic type coercion:

Boolean Evaluation

Values are not implicitly converted to booleans. Use explicit comparisons:

Edge Conditions

Edge conditions use CEL expressions without the {{}} wrapper:

Loop While Conditions

Loops use do-while semantics: the first iteration always executes, then iter.iteration increments before the while condition is checked to determine whether to continue.
Skipping the loop entirely: To conditionally skip a loop before it runs, use the node’s condition field:
Available namespaces in while:
  • outputs.* - Sub-workflow outputs defined in inline.outputs
  • iter.* - Loop iteration context (iter.iteration is 0-indexed)
  • inputs.* - Workflow inputs (useful for configurable iteration limits like inputs.max_turns)
Do-while behavior: The loop always runs at least once. After each iteration, iter.iteration increments, then the while condition is evaluated. Iteration counting: In the loop body, iter.iteration is 0-indexed (0, 1, 2…). In the while check, it reflects completed iterations (1 after first, 2 after second). Use iter.iteration < N to run exactly N iterations.

Common Mistakes

Missing Null Checks

Wrong Namespace

Using {{}} in Conditions

Empty String vs Null

Accessing Undefined Nodes


Debugging Tips

  1. Check node IDs: Ensure referenced node IDs match exactly (case-sensitive)
  2. Verify node completion: nodes.* only works after the node completes
  3. Use has() liberally: Wrap optional field access in has() checks
  4. Check types: Use type(x) to debug unexpected type errors
  5. Start simple: Build complex expressions incrementally

See Also