Relay expression language

While it's possible to write Relay workflows entirely using constant data, they're generally much more useful when they can handle dynamic inputs and outputs.

In certain parts of your workflow, you can use Relay's template language to access data dynamically. This includes:

Template expressions are delimited by ${ and }. For example, you may write the following in a step's spec:

  calculation: ${2 + 2}

When the step requests the calculation key from the spec, it will receive the value 4.

You can also use template expressions inside other strings. When you do this, the value produced by an expression will be automatically coerced to a string. Non-scalar types like maps and arrays will be converted to JSON. Here's a simple example:

  message: Looks like it's ${now()}. Time for a nap!

When the step requests the message key, it will receive a string value like "Looks like it's 2021-06-08T21:38:34Z. Time for a nap!"


Template expressions are generally designed to work in tandem YAML/JSON, and they support most of the same data types:


String literals are written using either single or double quotes. Use a backslash (\) to introduce an escape character.


Integers and floats are supported. When performing arithmetic or other operations that return numbers, a float value is always returned instead of an integer. This is similar to how JSON handles numbers.

Because we use floats extensively, they are subject to both precision issues for extremely large and extremely small values and inaccuracy due to rounding errors. If your workflow requires precise handling of numeric values (for example, working with money), you should encode your numeric values as strings and perform any required operations in your step code.


Use the identifiers true and false to represent boolean values.


Use the identifier null to represent the null value.

Maps (objects)

Maps are key-value pairs. Keys always have a string type, while values may be of any supported type. Map entries are not ordered.

You can construct a map literal using { and }, like a JSON object:

  map: ${{'foo': 'bar'}}


Arrays are ordered collections of values.

You can construct a map literal using [ and ], like in JSON:

  map: ${['foo', 1, 2, true]}



You can index into maps and arrays using either of two ways, a property syntax or a bracket syntax.

The property syntax uses dots to separate access. The bracket syntax puts the index to access inside square brackets, [ and ]. For example, given the following structure:

  "foo": {
    "bar": "baz",
    "quux": [true, 2, "hello"]
  "hello-to": "world"

Here are some possible ways to access data from it, and the resulting values:

  • # => "baz"
  • foo['bar'] # => "baz"
  • foo['hello-to'] # => "world"
  • foo.'hello-to' # => "world"
  • foo.quux[0] # => true
  • foo.quux.0 # => true
  • foo['quux'][1] # => 2
  • foo['quux'][1 + 1] # => "hello"
  • foo.quux.(1 + 1) # => "hello"

We don't provide an explicit recommendation on which indexing operators you should use. Use whatever is the most readable in your workflow. If you're constructing expressions programmatically, it's often easiest to use the bracket syntax as there is no possibility of ambiguity.


The following operators are supported:

  • Not: !, unary
  • Or: ||, binary
  • And: &&, binary

Operands will be coerced to a boolean before being applied. As a special case, the strings "true" and "TRUE" are considered to be equal to true and the strings "false" and "FALSE" are considered to be equal to false. Null is considered falsy.

All binary operators support short-circuiting.


The following operators are supported for all types:

  • Equal: ==, binary
  • Not equal: !=, binary

The following operators are supported for strings (lexicographically) and numbers:

  • Greater than: >, binary
  • Greater than or equal: >=, binary
  • Less than: <, binary
  • Less than or equal: <=, binary

Concatenation: +, binary

Two strings can be combined using the + operator. If either of the operands is not a string, it will be coerced to a string first using Go's %v formatter.


The following operators are supported for numbers:

  • Addition: +, binary
  • Subtraction: -, binary
  • Multiplication: *, binary
  • Division: /, binary
  • Modulus: %, binary (floating-point remainder, sign of dividend)
  • Exponentiation: **, binary

Pipeline: |>, binary

The binary pipeline operator allows you to chain expressions. It sets the input data for the following expression to the result of the preceding expression. For example:

  combined: ${jsonUnmarshal('{"a": 1, "b": 2}') |> a + b}

When the step requests the value of combined from the spec, it will get the value 3.


Parentheses allow you to evaluate an expression within another expression with the highest precedence. For example:

  calculation: ${(1 + 2) * 3}


Relay provides a small library of utility functions to make working with complex data a bit easier. When you've exhausted the possibilities of Relay's function library, you can often find what you need in a step, like our jq step.

The expression language supports two ways to call functions, positional or keyword argument invocation. Unlike other languages like Python, the argument patterns cannot be mixed in a single invocation.

For example, the merge function can be called as follows:

  • merge({'a': 'b'}, {'c': 'd'}) # => {"a": "b", "c": "d"}
  • merge(objects: [{'a': 'b'}, {'c': 'd'}], mode: 'shallow') # => {"a": "b", "c": "d"}

See the Relay function reference for the full list of functions and their usage.

Input data

When templates are evaluated, they are given input data by the workflow engine. This data allows you to make decisions about what the workflow should do.

The current top-level input data is always available as the value $. In fact, when you're really stuck, it can sometimes be useful to simply print the entire set of data available to you:

- name: everything
  image: alpine
    data: ${$}
  - ni get

If you're indexing into top-level data, you can omit the initial $. That is, $.foo and foo are equivalent ways to access the foo key from the input data map.

WARNING: If your account has connections or your workflow has secrets, this step will print all of that data to the log. Be careful to put this into a test workflow that you delete later or choose a subset of the data to print instead.

Relay always provides a map as the initial input data. Depending on what part of the workflow you're in, Relay makes some of the following keys available in the map:


Available in: Step and trigger specs, step env, step when conditions

You can reference a connection in your account using its type and name. Steps that require a connection to run look like this:

- name: describe-instances
  image: relaysh/ec2-step-describe-instances
      connection: ${'my-aws-account'}
      region: us-west-2


Available in: Step and trigger specs, step env, step when conditions

You can reference a secret stored in your workflow. For example:

- name: use-a-secret
  image: alpine
    pass: ${secrets.password}


Available in: Step specs, step env, step when conditions

You can reference a parameter provided to the current workflow run (manually or by a trigger binding). For example:

    description: A parameter the user needs to provide
    default: "A silly default value"

- name: use-a-parameter
  image: alpine
    userParam: ${parameters.myParameter}


Available in: Step specs, step env, step when conditions

You can reference outputs from previous steps. For example:

- name: make-output
  image: alpine
  - ni output set --key random --value "$(dd if=/dev/urandom bs=1 | head -c30 | base64)"

- name: use-output
  image: alpine
    stuff: ${outputs.'make-output'.random}

Generally, when you reference an output, Relay can automatically determine that it needs to create a dependency relationship with the step that is being referenced. For example, when you write ${}, Relay will automatically insert foo into the dependsOn for the step.

However, it is possible to write sufficiently complex expressions using outputs that this cannot be determined automatically. If you dynamically choose a step to reference, for example, you'll need to enumerate all of the possible dependencies in the step's dependsOn key by hand. You should always check the execution graph that Relay generates to be sure it meets your expectations.


Available in: Trigger when conditions, trigger bindings

You can reference data from an event sent to a Relay trigger. For example:

- name: github
    type: webhook
    image: relaysh/datadog-trigger-event-fired
      what: ${event.title}

    description: The thing to alert on