Skip to content

Templating

Most fields on a step accept {{ }} templates, and every template can reference the same data. Each field that accepts templates has a variable picker. After you test a step, the picker shows a real example value next to every path that step produces.

ReferenceWhat it holds
{{ trigger.* }}The trigger payload. Its shape depends on the trigger. See Triggers.
{{ steps.<id>.output.* }}What an earlier step returned.
{{ steps.<id>.error }}The error message, when that step is set to continue on failure.
{{ item }} and {{ item_index }}The current element inside a loop, and its position.
{{ vars.<name> }}A workspace constant. See Variables.
{{ secrets.<name> }}A workspace secret. See Variables.

The builder generates a step’s id when you add it, and renaming the step doesn’t change it. Don’t type these references. Pick them from the variable picker, which lists steps under the names you gave them and inserts the right id. Examples on these pages use readable ids such as steps.lookup to show the shape.

Templates use Liquid. {{ }} outputs a value, {% %} runs logic such as an if or a for loop, and a filter after a | changes a value before it’s output. The standard filters and tags all work, apart from tags that load another file, such as include. The LiquidJS filter reference and tag reference list them with examples.

A template string can be up to 8192 characters long.

Mix references with plain text:

Bearer {{ secrets.stripe_key }}

When a field might be missing, give it a fallback with default:

Service: {{ trigger.body.group.labels.service | default: "unknown" }}

Show different text depending on a condition:

{{ trigger.body.group.title }}: {% if trigger.body.group.severity == "sev1" %}page the on-call engineer.{% else %}review it during working hours.{% endif %}

Write a line for each item in a list:

{% for rule in trigger.body.rules %}
- {{ rule.alarm_name }}: {{ rule.instances.size }} instances
{% endfor %}

tz is a KloudMate filter that shows a timestamp in the timezone you name, or in UTC if you don’t name one. With an opened_at of 2026-05-23T10:12:00Z, this prints Opened 2026-05-23 06:12:00 EDT:

Opened {{ trigger.body.group.opened_at | tz: "America/New_York" }}

Turn a comma-separated constant into a list for a JSON field:

{{ vars.regions | split: "," }}
  • A field is a template only when it contains {{. A value made only of {% %} tags is used exactly as written. To choose a value with tags alone, assign it and output it at the end, as in {% if x %}{% assign p = "High" %}{% else %}{% assign p = "Low" %}{% endif %}{{ p }}.
  • A lone reference keeps its type. When a field holds one reference and nothing else, such as {{ trigger.body }} or {{ item.pods }}, the step gets the value itself: an object stays an object, and a list stays a list. Anything else renders as text, with an object or a list inside it written out as JSON.
  • A loop’s Items takes a plain reference. A filter turns the list into text, and a loop fails when Items isn’t a list.
  • 0 and empty text count as false. {% if count %} is false when count is 0, and default replaces 0 as well as a missing value. To check whether a value is set at all, compare it with nil, as in {% if count != nil %}.
  • Write an array position with a dot, as in trigger.body.rules.0.instances.0.labels. [0] also works in a template on an action’s field, but condition fields and a loop’s Items read rules[0] as a key name. Publishing refuses [0] there and suggests the dot form.
  • Put a key with spaces or punctuation in quoted brackets, as in {{ trigger.body["Alert Name"] }}. This works in condition fields too.
  • Publishing parses every template. A syntax error or a filter that doesn’t exist stops the publish, and the error names the field.

The variable picker writes array positions and quoted keys in the right form for you.

Send Email accepts HTML in its Message, and anyone who has a workflow’s webhook URL can write the payload. A payload value placed straight into the message can add markup of its own, such as a link that points somewhere else. Pass every value you didn’t write yourself through escape, including payload fields, command output, resource names, and error messages:

Alert: <b>{{ trigger.body.group.title | escape }}</b>
Labels: {{ trigger.body.group.labels | json | escape }}

For a list or an object, add json before escape. Without it, escape prints an object as [object Object].

escape changes only HTML characters (<, >, &, and quotes), so Markdown in the value, including a link, still renders. The subject line is plain text and needs no escaping, and KloudMate escapes the messages of Approval and Collect Input steps for you.

An unknown reference renders empty rather than failing. The Test panel lists every path that resolved to nothing under Unresolved variables, and the run timeline records the same list per step, so check both. The list includes a reference with a default too, so you can see when it used the fallback.

Publishing does reject a reference to a step that doesn’t exist, and names the step.