CEL Expressions Reference
CEL Expressions Reference
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
- Template Interpolation: Use
{{expression}}to insert dynamic values into strings - Pure Expressions: When an entire value is
{{expression}}, the native type is preserved (not converted to string) - Literal Strings: Text without
{{}}is treated as a literal value - Explicit Namespaces: All data access uses explicit namespaces (
inputs.*,nodes.*, etc.)
Quick Examples
# Simple value interpolation
model: "{{inputs.model}}"
# Literal string (no interpolation)
role: user
# String with embedded expression
content: "Processing iteration {{iter.iteration + 1}} of {{iter.max}}"
# Conditional expression (ternary)
model: "{{inputs.model != '' ? inputs.model : 'claude-4-sonnet'}}"
# Accessing node outputs
tool_calls: "{{nodes.call_llm.tool_calls}}"Template Syntax
Basic Interpolation
Use double curly braces to embed expressions within strings:
content: "Hello, {{inputs.username}}!"Multiple expressions can appear in a single string:
content: "Running {{iter.iteration + 1}} of {{iter.max}} iterations"Type Preservation
When an entire YAML value is a single {{expression}}, the expression’s native type is preserved:
# Returns array (native type preserved)
tool_calls: "{{nodes.call_llm.tool_calls}}"
# Returns integer (native type preserved)
max_turns: "{{inputs.max_turns}}"
# Returns string (interpolation converts to string)
message: "You have {{nodes.count.value}} items"Literal Values
Text without {{}} is passed through as-is:
role: user
action: CallLLM
content: This is a literal string with no expressionsAvailable 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.
inputs:
model:
type: model
default: ""
temperature:
type: number
default: 1.0
mode:
type: enum
enum: ["manual", "agent", "plan"]
default: "agent"
nodes:
- id: call_llm
action: CallLLM
inputs:
model: "{{inputs.model}}"
temperature: "{{inputs.temperature}}"Common patterns:
# Direct access
"{{inputs.model}}"
# With default fallback
"{{inputs.model != '' ? inputs.model : 'claude-4-sonnet'}}"
# Checking boolean inputs
"{{inputs.mode == 'agent'}}"
# Accessing grouped inputs
"{{inputs.Implementer.temperature}}"nodes.*
Accesses outputs from completed workflow nodes. Only available after the referenced node has completed.
# Access a node's output field
"{{nodes.call_llm.response_text}}"
# Access nested fields
"{{nodes.call_llm.message.role}}"
# Access tool calls array
"{{nodes.call_llm.tool_calls}}"
# Check if a node completed with specific value
condition: nodes.run_tests.exit_code == 0Common CallLLM outputs:
| Field | Type | Description |
|---|---|---|
message.role | string | Always "assistant" |
message.text | string | Response text content |
response_text | string | Same as message.text |
tool_calls | array | Tool calls requested by model |
input_tokens | int | Input tokens consumed |
output_tokens | int | Output tokens generated |
Common Run outputs (shell commands):
| Field | Type | Description |
|---|---|---|
exit_code | int | Command exit code (0 = success) |
stdout | string | Standard output |
stderr | string | Standard error |
Loop outputs:
| Field | Type | Description |
|---|---|---|
iterations | int | Number of iterations completed |
max | int | Maximum iterations configured |
succeeded | bool | Whether loop completed successfully |
thread.*
Provides access to the current thread context.
| Field | Type | Description |
|---|---|---|
thread.id | string | Thread identifier (UUID) |
thread.mode | string | Thread mode (new, inherit, fork) |
thread.forked_from | string | Parent thread ID if forked |
thread.messages | array | All messages in the thread |
thread.last_message | object | Most recent message |
# Reference thread ID for activities
inputs:
thread: "{{thread.id}}"
# Access last message content
content: "Previous message: {{thread.last_message.content}}"
# Check if thread has messages
condition: size(thread.messages) > 0workflow.*
Provides workflow metadata and execution context.
| Field | Type | Description |
|---|---|---|
workflow.id | string | Unique workflow execution ID |
workflow.name | string | Workflow definition name |
workflow.path | string | Working directory path |
workflow.branch | string | Current git branch |
workflow.mode | string | Execution mode from inputs |
# Create unique resource names
name: "worktree-{{workflow.id}}"
# Include workflow context in messages
content: "Workflow {{workflow.name}} completed"
# Access git branch
base_branch: "{{workflow.branch}}"trigger.*
Provides data about the event that triggered the current node.
| Field | Type | Description |
|---|---|---|
trigger.node | string | ID of the node that triggered this one |
trigger.id | string | Event ID |
trigger.entity_id | string | Entity ID (e.g., tool_call_id) |
trigger.message.role | string | Trigger message role |
trigger.message.content | string | Trigger message content |
trigger.thread.id | string | Thread ID from trigger |
trigger.attachments | array | Attachment IDs |
# Access the user's original request
content: "Implement this: {{trigger.message.content}}"
# Reference in inject messages
thread:
inject:
role: user
content: "Process: {{trigger.message.content}}"iter.*
Provides loop iteration context. Only available inside loop constructs.
| Field | Type | Description |
|---|---|---|
iter.iteration | int | Current iteration (0-indexed) |
iter.max | int | Maximum iterations |
iter.previous | object | Outputs from previous iteration |
# Display human-readable iteration number
content: "Attempt {{iter.iteration + 1}} of {{iter.max}}"
# Access previous iteration's output
content: |
{{iter.iteration == 0 ?
trigger.message.content :
'Previous attempt failed: ' + iter.previous.stderr}}
# Use in loop until condition
until: outputs.exit_code == 0 || iter.iteration >= 5output.*
Used in save_message blocks to reference the current activity’s output.
- id: call_llm
action: CallLLM
save_message:
role: "{{output.message.role}}"
content: "{{output.message.text}}"
tool_calls: "{{output.tool_calls}}"
input_tokens: "{{output.input_tokens}}"
output_tokens: "{{output.output_tokens}}"outputs.*
Used in loop until conditions to reference the sub-workflow’s declared outputs.
loop:
max: 10
until: outputs.exit_code == 0
inline:
outputs:
exit_code: "{{nodes.verify.exit_code}}"Built-in Functions
Standard CEL Functions
| Function | Description | Example |
|---|---|---|
size(x) | Length of list, map, or string | size(nodes.call_llm.tool_calls) > 0 |
has(x.field) | Check if field exists | has(nodes.audit) ? nodes.audit.passed : true |
type(x) | Get type of value | type(inputs.data) == "string" |
String Functions
| Function | Description | Example |
|---|---|---|
contains(s, substr) | Check if string contains substring | contains(output.stderr, "error") |
startsWith(s, prefix) | Check if string starts with prefix | startsWith(inputs.model, "claude") |
endsWith(s, suffix) | Check if string ends with suffix | endsWith(file, ".go") |
matches(s, pattern) | Regex match | matches(output.stdout, "^OK") |
Reliant Custom Functions
first(list)
Returns the first element of a list, or null if empty.
first_tool: "{{nodes.call_llm.tool_calls.first()}}"last(list)
Returns the last element of a list, or null if empty.
last_message: "{{thread.messages.last()}}"join(list, delimiter)
Joins list elements into a string with a delimiter.
# Join array elements
tools_list: "{{inputs.tools.join(',')}}"parseJson(string)
Parses a JSON string into a CEL value (map or list).
data: "{{parseJson(nodes.fetch.body)}}"toJson(value)
Converts a value to its JSON string representation.
# As global function
json_str: "{{toJson(nodes.call_llm.tool_calls)}}"
# As member function
json_str: "{{nodes.call_llm.tool_calls.toJson()}}"coalesce(value1, value2, ...)
Returns the first non-null argument. Supports 2-4 arguments.
# Use default if input is null
model: "{{coalesce(inputs.model, 'claude-4-sonnet')}}"
# Chain multiple fallbacks
value: "{{coalesce(inputs.primary, inputs.secondary, 'default')}}"getOrDefault(map, key, default)
Safely accesses a map key with a fallback default value.
name: "{{getOrDefault(inputs, 'name', 'unnamed')}}"responseData(tool_results, tool_name)
Extracts parsed metadata from a response tool result.
audit_result: "{{responseData(nodes.execute_tools.tool_results, 'audit_result')}}"now()
Returns the current UTC timestamp as an ISO 8601 string.
timestamp: "{{now()}}"
# Returns: "2025-01-15T10:30:00Z"parseDuration(string)
Parses a Go duration string and returns seconds as a number.
timeout_seconds: "{{parseDuration('5m')}}" # Returns: 300
total_seconds: "{{parseDuration('1h30m')}}" # Returns: 5400Common Patterns
Conditional Logic
Use the ternary operator for conditional values:
# Simple conditional
model: "{{inputs.model != '' ? inputs.model : 'claude-4-sonnet'}}"
# Nested conditional
prompt: "{{inputs.mode == 'plan' ? inputs.planning_prompt : inputs.system_prompt}}"
# Multi-level conditional
content: |
{{iter.iteration == 0 ? trigger.message.content :
iter.iteration == 1 ? 'First attempt failed: ' + iter.previous.stderr :
'Multiple attempts failed: ' + iter.previous.stderr}}Default Values
Several patterns for handling missing or empty values:
# Ternary with empty string check
model: "{{inputs.model != '' ? inputs.model : 'default-model'}}"
# Using coalesce (for null values)
value: "{{coalesce(inputs.optional, 'fallback')}}"
# Using getOrDefault (for map access)
setting: "{{getOrDefault(inputs, 'setting', 'default')}}"
# Combining approaches
model: "{{inputs.Custom.model != '' ? inputs.Custom.model : coalesce(inputs.model, 'default')}}"Checking Optional Fields
Use has() to safely check for optional fields:
# Check if field exists before accessing
condition: "{{has(nodes.audit) ? nodes.audit.passed : true}}"
# Check nested optional fields
branch: "{{has(workflow.branch) ? workflow.branch : 'main'}}"Working with Tool Calls
Common patterns for handling tool calls from LLM responses:
# Check if there are tool calls
condition: nodes.call_llm.tool_calls != null && size(nodes.call_llm.tool_calls) > 0
# In edge conditions
edges:
- from: call_llm
cases:
- to: execute_tools
condition: nodes.call_llm.tool_calls != null && size(nodes.call_llm.tool_calls) > 0
- to: done
label: "no_tools"Loop Iteration Patterns
Using iteration context for progressive retry logic:
content: |
{{iter.iteration == 0 ?
trigger.message.content :
'Previous attempt failed. Error:\n' + iter.previous.stderr + '\n\nTry again.'}}String Interpolation
Embed multiple values in strings:
content: |
## Task Complete
**Iterations:** {{nodes.loop.iterations}}
**Status:** {{nodes.loop.succeeded ? 'Success' : 'Failed'}}
**Exit Code:** {{nodes.verify.exit_code}}Type Handling
CEL Type System
CEL is strongly typed. Values have specific types that affect how operators and functions work.
| Type | Description | Examples |
|---|---|---|
int | Integer numbers | 0, 42, -1 |
double | Floating point | 1.5, 0.0, -3.14 |
string | Text | "hello", "" |
bool | Boolean | true, false |
list | Ordered collection | [1, 2, 3], [] |
map | Key-value pairs | {"key": "value"} |
null | Null value | null |
Null Checks
Always check for null before accessing nested fields:
# Safe null check pattern
condition: nodes.call_llm.tool_calls != null && size(nodes.call_llm.tool_calls) > 0
# Using has() for optional fields
value: "{{has(nodes.optional_node) ? nodes.optional_node.result : 'default'}}"
# Coalesce for null handling
model: "{{coalesce(inputs.model, 'default')}}"Type Coercion
CEL performs limited automatic type coercion:
# Numbers in string interpolation are converted to strings
content: "Count: {{nodes.count.value}}" # int -> string
# Explicit string conversion if needed
str_value: "{{string(nodes.count.value)}}"Boolean Evaluation
Values are not implicitly converted to booleans. Use explicit comparisons:
# Correct: explicit comparison
condition: nodes.run.exit_code == 0
# Correct: explicit null check
condition: nodes.call_llm.tool_calls != null
# Wrong: implicit boolean conversion (will error)
# condition: nodes.run.exit_code # Don't do thisEdge Conditions
Edge conditions use CEL expressions without the {{}} wrapper:
edges:
- from: call_llm
cases:
# Condition is a raw CEL expression (no {{}} needed)
- to: execute_tools
condition: nodes.call_llm.tool_calls != null && size(nodes.call_llm.tool_calls) > 0
label: "has_tools"
# Combining multiple conditions
- to: approval
condition: nodes.call_llm.tool_calls != null && size(nodes.call_llm.tool_calls) > 0 && inputs.mode == 'manual'
label: "require_approval"
# Default case (no condition or always true)
- to: done
label: "complete"Loop Until Conditions
The until clause in loops uses CEL to determine when to stop:
loop:
max: 10
until: outputs.exit_code == 0 # Raw CEL expression
inline:
outputs:
exit_code: "{{nodes.verify.exit_code}}"Available namespaces in until:
outputs.*- Sub-workflow outputs defined ininline.outputsiter.*- Loop iteration contextthread.*- Thread context
Common Mistakes
Missing Null Checks
# Wrong: will error if tool_calls is null
condition: size(nodes.call_llm.tool_calls) > 0
# Correct: check for null first
condition: nodes.call_llm.tool_calls != null && size(nodes.call_llm.tool_calls) > 0Wrong Namespace
# Wrong: 'message' is not a top-level namespace
content: "{{message.content}}"
# Correct: use trigger.message for trigger data
content: "{{trigger.message.content}}"
# Correct: use thread.last_message for thread messages
content: "{{thread.last_message.content}}"Using {{}} in Conditions
# Wrong: don't use {{}} in edge conditions
condition: "{{nodes.run.exit_code == 0}}"
# Correct: raw CEL expression in conditions
condition: nodes.run.exit_code == 0Empty String vs Null
# Empty string is NOT null - coalesce won't help
model: "{{coalesce(inputs.model, 'default')}}" # Returns "" if inputs.model is ""
# Use ternary for empty string check
model: "{{inputs.model != '' ? inputs.model : 'default'}}"Accessing Undefined Nodes
# Wrong: node hasn't completed yet or doesn't exist
content: "{{nodes.nonexistent.value}}"
# Correct: use has() for optional nodes
content: "{{has(nodes.optional) ? nodes.optional.value : 'N/A'}}"Debugging Tips
- Check node IDs: Ensure referenced node IDs match exactly (case-sensitive)
- Verify node completion:
nodes.*only works after the node completes - Use has() liberally: Wrap optional field access in
has()checks - Check types: Use
type(x)to debug unexpected type errors - Start simple: Build complex expressions incrementally
See Also
- Workflow Schema Reference - Complete workflow YAML schema
- Activities Reference - Activity inputs and outputs
- CEL Language Specification - Official CEL documentation