Test Workflows – Expressions
Our Test Workflows system uses a simple, yet powerful, expressions language to dynamically evaluate and transform values at runtime. This language is built on JSON and supports basic arithmetic, logical operations, and many useful built-in functions, enabling you to customize your workflows flexibly.
Overview
-
JSON-Native:
Since the language is based on JSON, every valid JSON value is also a valid expression. For example,[ "a", "b", "c" ]is a valid expression. -
Mathematical Operations:
You can perform arithmetic calculations directly within expressions. For instance, you can multiply a configuration parameter by a number like so:config.workers * 5 -
Dynamic Evaluation:
Expressions are used to evaluate conditions, dynamically set image tags, generate scripts, and more. They allow you to manipulate values on the fly during workflow execution.
Examples
Defining a Condition
You can define conditions using logical and arithmetic operators. For example, this condition checks if there is more than one worker or if a force flag is set:
condition: "config.workers > 1 || config.force"
Dynamic Image Tag
By embedding expressions within double curly brackets {{ }}, you can dynamically generate values. In the example below, the container image tag is determined by the configuration parameter version:
image: "mcr.microsoft.com/playwright:v{{ config.version }}"
Generating Array values
By surrounding embedded expressions within brackets [ ], you can dynamically generate array property values. In the example below, the git.content paths are taken from a comma-separate config value:
content:
git:
uri: https://github.com/kubeshop/testkube
revision: main
paths: ["{{ split(config.paths) }}"]
Configurable K6 Script
Expressions can also be used to build dynamic content files. For example, you can create a K6 script that uses JSON-parsed thresholds and additional script content from your configuration:
content:
files:
- path: /k6-script.js
content: |
export const options = {
thresholds: {{ json(config.thresholds) }}
};
{{ config.script }}
Operators
The expressions language supports a variety of operators. They follow standard mathematical rules for precedence.
Arithmetic Operators
-
Addition (
+) and Subtraction (-):
Perform addition or subtraction on numbers. They also allow string concatenation.Examples:
1 + 2 * 3evaluates to7(1 + 2) * 3evaluates to9"foo" + "bar"evaluates to"foobar"
-
Multiplication (
*), Division (/), and Modulo (%):
Multiply, divide, or find the remainder between numbers.Examples:
4 * 2evaluates to810 / 4evaluates to2.55 % 3evaluates to2
-
**Exponentiation (
**):**
Raise a number to the power of another.Example:
2 ** 5evaluates to32
-
Parentheses (
(and)):
Group expressions to control the order of evaluation.Example:
(2 + 3) * 5evaluates to20
Comparison and Logical Operators
| Operator | Returns | Description | Example |
|---|---|---|---|
== or = |
bool |
Checks equality | 3 == 5 is false |
!= or <> |
bool |
Checks inequality | 3 != 5 is true |
> |
bool |
Greater than | 3 > 5 is false |
< |
bool |
Less than | 3 < 5 is true |
>= |
bool |
Greater than or equal to | 3 >= 5 is false |
<= |
bool |
Less than or equal to | 3 <= 5 is true |
&& |
bool |
Logical AND (returns the last truthy or first falsy value) | true && false is false |
|| |
bool |
Logical OR (returns the first truthy or last value) | 0 || 5 is 5 |
! |
bool |
Logical NOT | !0 is true |
? : |
any | Ternary operator (if/else) | true ? 5 : 3 is 5 |
Access Operators
These operators allow you to access elements within data structures:
-
.: Access a property or element
Example:{"id": 10}.idreturns10 -
.*.: Wildcard mapping
Example:[{"id": 5}, {"id": 3}].*.idreturns[5, 3] -
...: Spread arguments
Example:shellquote(["foo", "bar baz"]...)is equivalent toshellquote("foo", "bar baz")
Built-in Variables
The expressions language provides several built-in variables. Some of these are resolved before execution (and can be used in Pod settings), while others are available only dynamically within the container.
General Variables
| Name | Resolved Immediately | Description |
|---|---|---|
always |
✅ | Alias for true |
never |
✅ | Alias for false |
config (e.g., config.abc) |
✅ | User-provided configuration values |
execution.id |
✅ | ID of the current TestWorkflow execution |
execution.name |
✅ | Name of the current TestWorkflow execution |
execution.number |
✅ | Sequence number of the execution |
execution.tags.<name> |
✅ | Tags passed to the execution with --tag |
execution.scheduledAt |
✅ | Scheduled execution date/time |
execution.lineage.baseId |
✅ | ID of the execution this one is a rerun of; empty on an original run - see Rerunning Failed Tests |
execution.lineage.rootId |
✅ | ID of the first execution in the chain of reruns; an original run's own ID |
execution.lineage.attempt |
✅ | 1 on an original run, one more than the base's on a rerun |
resource.id |
✅ | Unique ID for parallel steps or services |
resource.root |
✅ | Resource ID of the parent resource |
namespace |
✅ | Namespace where the execution runs |
workflow.name |
✅ | Name of the executed TestWorkflow |
organization.id |
✅ | Organization ID (when using Control Plane) |
environment.id |
✅ | Environment ID (when using Control Plane) |
dashboard.url |
✅ | URL of the environment’s Dashboard |
labels (e.g., labels.some_label_key) |
✅ | Labels assigned to the TestWorkflow (special characters are normalized) |
env (e.g., env.SOME_VARIABLE) |
❌ | Environment variable values (resolved at runtime) |
failed |
❌ | Indicates if the execution has already failed |
passed |
❌ | Indicates if the execution has not yet failed |
services (e.g., services.db.0.ip) |
❌ | IP addresses of initialized services |
step.results |
❌ | Path to the current step's results directory (for writing files that later steps can read) |
step.<id>.results |
❌ | Path to a previous step's results directory (for reading files written by that step) |
step.<id>.outputs.<key> |
❌ | Output value written by a previous step to /testkube/outputs/<key> |
See Sharing Data Between Steps for usage examples.
Contextual Variables
Additional variables are available in specific contexts:
Retry Conditions
When defining a custom retry condition, you can use self.passed and self.failed to determine the step status:
spec:
steps:
- shell: exit 0
# Retry until the step has failed, for up to 5 executions
retry:
count: 5
until: "self.failed"
Matrix and Shard
When running steps in parallel (via services, parallel, or execute steps), you can access:
matrix.<name>andshard.<name>for custom parametersindexandcountfor each copymatrixIndex,matrixCount,shardIndex, andshardCountfor detailed indexing
Example using matrix values:
spec:
services:
# Deploy two workers with index information
db:
count: 2
description: "Instance {{ index + 1 }} of {{ count }}" # "Instance 1 of 2" and "Instance 2 of 2"
image: mongo:latest
# Deploy two servers with different Node.js versions
api:
matrix:
node: [20, 21]
description: "Node v{{ matrix.node }}" # "Node v20" and "Node v21"
image: "node:{{ matrix.node }}"
Built-in Functions
The expressions language includes many functions that help manipulate and cast values.
Casting Functions
| Function | Returns | Description | Example |
|---|---|---|---|
string |
string | Casts a value to a string | string(5) returns "5" |
list |
list | Creates a list from provided values | list(10, 20) returns [10, 20] |
int |
int | Converts a value to an integer | int(10.5) returns 10 |
bool |
bool | Converts a value to a boolean | bool("") returns false |
float |
float | Converts a value to a floating-point number | float("300.50") returns 300.5 |
eval |
any | Evaluates an expression | eval("4 * 5") returns 20 |
General Functions
| Function | Returns | Description | Example |
|---|---|---|---|
join |
string | Joins list elements into a single string | join(["a", "b"]) returns "a,b"; join(["a", "b"], " - ") returns "a - b" |
split |
list | Splits a string into a list based on a delimiter | split("a,b,c") returns ["a", "b", "c"] |
trim |
string | Removes whitespace from both ends of a string | trim(" \nabc d ") returns "abc d" |
len |
int | Returns the length of an array, map, or string | len(["a", "b"]) returns 2 |
floor |
int | Rounds a number down to the nearest integer | floor(10.5) returns 10 |
ceil |
int | Rounds a number up to the nearest integer | ceil(10.5) returns 11 |
round |
int | Rounds a number to the nearest integer | round(10.5) returns 11 |
at |
any | Retrieves an element from a list or map by index/key | at([10, 2], 1) returns 2; at({"foo": "bar"}, "foo") returns "bar" |
tojson |
string | Serializes a value to JSON format | tojson({ "foo": "bar" }) returns "{\"foo\":\"bar\"}" |
json |
any | Parses a JSON string into an object | json("{\"foo\":\"bar\"}") returns { "foo": "bar" } |
toyaml |
string | Serializes a value to YAML format | toyaml({ "foo": "bar" }) returns a YAML string |
yaml |
any | Parses a YAML string into an object | yaml("foo: bar") returns { "foo": "bar" } |
shellquote |
string | Sanitizes and quotes arguments for safe shell usage | shellquote("foo bar") returns "\"foo bar\"" |
shellparse |
list | Parses a shell command string into an array of arguments | shellparse("foo bar") returns ["foo", "bar"] |
map |
list or map | Applies an expression to each element in a list or map | map([1,2,3,4,5], "_.value * 2") returns [2,4,6,8,10] |
filter |
list | Filters a list using an expression predicate | filter([1,2,3,4,5], "_.value > 2") returns [3,4,5] |
jq |
any | Executes a jq expression on a value | jq([1,2,3,4,5], ". | max") returns 5 |
range |
list | Generates a range of numbers | range(5, 10) returns [5, 6, 7, 8, 9]; range(5) returns [0, 1, 2, 3, 4] |
relpath |
string | Computes a relative file path | relpath("/a/b/c") might return ./b/c |
abspath |
string | Computes an absolute file path | abspath("b/c") might return /some/working/dir/b/c |
chunk |
list | Splits a list into chunks of a specified maximum size | chunk([1,2,3,4,5], 2) returns [[1,2], [3,4], [5]] |
date |
string | Returns the current date in a specified format | date() might return "2024-06-04T11:59:32.308Z"; date("2006-01-02") might return "2024-06-04" |
secret |
string | Creates a reference to a secret as an environment variable | secret("name", "key") returns a reference like "env.S_N_name_K_key" |
File System Functions
| Function | Returns | Description | Example |
|---|---|---|---|
file |
string | Returns the contents of a file | file("/etc/some/path") might return "some\ncontent" |
glob |
list | Finds files matching a pattern | glob("/etc/**/*", "./x/**/*.js") might return ["/etc/some/file", "/etc/other/file", "/some/working/dir/x/file.js"] |
Execution Data Functions
These read data produced by other Test Workflow executions - the ones the current Workflow ran with execute, the one that ran it, and its siblings. See Sharing Data Between Executions for the full picture.
| Function | Returns | Description | Example |
|---|---|---|---|
execution |
map | Returns data about another execution - its id, name, workflow, alias, index, status and outputs. Takes a reference and an optional position within a fan-out group |
execution("p").outputs.token might return "abc123"; execution("shards", 2).status might return "passed" |
read_artifact |
string | Returns the contents of a single artifact of another execution, up to 1 MiB | read_artifact("parent", "fixtures/data.json") might return '{"cases":3}' |
The reference is an as alias, a Workflow name, an execution ID, or one of two reserved words - "parent" for the execution that ran the current one, and "rerun" for the execution the current one is a rerun of:
- shell: |
echo 'sibling token: {{ execution(config.producerId).outputs.token }}'
echo 'parent seed: {{ execution("parent").outputs.seed }}'
"rerun" resolves only on a rerun. On an original run it has nothing to point at, and the step fails while its expressions are being resolved - whichever branch its script would have taken. So it belongs in a step of its own, guarded by a condition:
- name: Read the report of the run this is a rerun of
condition: 'execution.lineage.baseId != ""'
shell: |
printf '%s' {{ shellquote(read_artifact("rerun", "junit/report.xml")) }} > /data/previous.xml
A report is arbitrary text produced by your tests, so it goes through shellquote rather than straight into a quoted string: a single quote anywhere in it would otherwise close the string it was substituted into and leave the remainder to be read as shell syntax. See Rerunning Failed Tests for what to do with it.
Example using file and glob functions:
condition: 'len(glob("/data/repo/**/*.spec.js")) > 0'
Another example combining file reading and command generation:
- shell: "generate-api-key.sh > /data/api-key"
- execute:
workflows:
- name: example
config:
entries: '{{ tojson(split(file("/data/list*"), "\n")) }}'
script: '{{ file("/data/api-key") }}'
You can also use functions like shellquote to safely pass arguments to shell commands:
shell: |
/bin {{ shellquote("--", config.args ...) }}
By leveraging the Test Workflows expressions language, you can build dynamic, configurable workflows that perform complex evaluations and manipulations at runtime. For additional details on operators, built-in functions, and contextual variables, please refer back to this document whenever needed.