CEL Expressions
CEL (Common Expression Language) is the language you use in kro to reference data between resources, compute values, and define conditions. Understanding CEL is essential for creating ResourceGraphDefinitions.
What is CEL?
CEL (Common Expression Language) is an open-source expression language originally created by Google. It's the same language Kubernetes uses for validation rules, admission control, and field selectors.
Why CEL is Safe
CEL was designed specifically to be safe for executing user code. Unlike scripting languages where you'd never blindly execute user-provided code, you can safely execute user-written CEL expressions. This safety comes from:
- No side effects: CEL expressions can't modify state, write files, or make network calls
- Guaranteed termination: No loops or recursion means expressions always complete
- Resource bounded: Expressions are prevented from consuming excessive memory or CPU
- Sandboxed execution: CEL can't access the filesystem or system resources
Why CEL is Fast
CEL is optimized for compile-once, evaluate-many workflows:
- Parse and check expressions once at configuration time (when you create an RGD)
- Store the checked AST (Abstract Syntax Tree)
- Evaluate the stored AST repeatedly at runtime against different inputs
Because CEL prevents behaviors that would make it slower, expressions evaluate in nanoseconds to microseconds - making it ideal for performance-critical reconciliation loops.
Learn more: CEL Overview | CEL Language Specification | CEL Go Documentation
CEL Syntax in kro
Expression Delimiters
In kro, CEL expressions are wrapped in ${ and }:
metadata:
name: ${schema.spec.appName}
Everything between ${ and } is a CEL expression that gets evaluated at runtime.
Two Types of Expressions
1. Standalone Expressions
A standalone expression is a field whose value is exactly one expression - nothing else:
spec:
replicas: ${schema.spec.replicaCount}
The expression result replaces the entire field value. The result type must match the field's expected type:
- If the field expects an integer, the expression must return an integer
- If the field expects an object, the expression must return an object
- etc.
Examples:
# Integer field
replicas: ${schema.spec.count}
# String field
image: ${schema.spec.containerImage}
# Boolean field
enabled: ${schema.spec.featureEnabled}
# Object field
env: ${configmap.data}
# Array field
volumes: ${schema.spec.volumeMounts}
2. String Templates
A string template contains one or more expressions embedded in a string:
metadata:
name: "${schema.spec.prefix}-${schema.spec.name}"
All expressions in a string template must return strings, and the result is always a string (concatenation of all parts).
Examples:
# Simple concatenation
name: "app-${schema.spec.name}"
# Multiple expressions
connectionString: "host=${database.status.endpoint}:${database.status.port}"
# With literal text
message: "Application ${schema.spec.name} is running version ${schema.spec.version}"
Expressions in string templates must return strings. This won't work:
name: "app-${schema.spec.replicas}" # Error: replicas is an integer
Use string() to convert:
name: "app-${string(schema.spec.replicas)}"
Referencing Data
The schema Variable
The schema variable represents the instance spec - the values users provide when creating an instance of your API.
Instance:
apiVersion: kro.run/v1alpha1
kind: WebApplication
metadata:
name: my-app
spec:
appName: awesome-app
replicas: 3
In your RGD, access via schema.spec:
resources:
- id: deployment
template:
metadata:
name: ${schema.spec.appName} # "awesome-app"
spec:
replicas: ${schema.spec.replicas} # 3
Resource Variables
Each resource in your RGD can be referenced by its id:
resources:
- id: deployment
template:
apiVersion: apps/v1
kind: Deployment
# ... deployment spec
- id: service
template:
apiVersion: v1
kind: Service
spec:
selector:
# Reference the deployment's labels
app: ${deployment.spec.template.metadata.labels.app}
This automatically creates a dependency: the service depends on the deployment. kro will create the deployment first. See Dependencies & Ordering for details.
Field Paths
Use dot notation to navigate nested fields:
# Access nested objects
${deployment.spec.template.spec.containers[0].image}
# Access map values
${configmap.data.DATABASE_URL}
# Access status fields
${deployment.status.availableReplicas}
Array Indexing
Access array elements using [index]:
# First container's image
${deployment.spec.template.spec.containers[0].image}
# Second port
${service.spec.ports[1].port}
The Optional Operator (?)
The ? operator makes a field access optional. If the field doesn't exist, the expression returns null instead of failing.
When to Use ?
Use the optional operator when:
- Referencing schema-less objects (ConfigMaps, Secrets without known structure)
- Accessing fields that might not exist (optional status fields)
- Working with dynamic data where structure isn't guaranteed
Syntax
Place ? before the field that might not exist:
${configmap.data.?DATABASE_URL}
If data.DATABASE_URL doesn't exist, this returns null instead of erroring.
Examples
Referencing a ConfigMap:
resources:
- id: config
externalRef:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
- id: deployment
template:
spec:
containers:
- env:
- name: DATABASE_URL
# ConfigMap might not have this key
value: ${config.data.?DATABASE_URL}
Optional status fields:
# Some resources might not have this status field immediately
ready: ${deployment.status.?readyReplicas > 0}
Chaining optional accessors:
# Multiple fields might not exist
${service.status.?loadBalancer.?ingress[0].?hostname}
The ? operator prevents kro from validating the field's existence at build time. Use it sparingly - prefer explicit schemas when possible.
Available CEL Libraries
| Library | Documentation |
|---|---|
| Lists | cel-go/ext |
| Strings | cel-go/ext |
| Encoders | cel-go/ext |
| Random | kro custom |
For the complete CEL language reference, see the CEL language definitions.
Type Checking and Validation
One of kro's key features is compile-time type checking of CEL expressions.
How Type Checking Works
When you create an RGD, kro:
- Fetches the OpenAPI schema for each resource type from the API server
- Validates that every field path in your expressions exists
- Checks that expression output types match target field types
- Reports errors before any instances are created
Example:
spec:
replicas: ${schema.spec.appName} # Error: appName is string, replicas expects integer
kro will reject this RGD with a clear error message.