Pebble Syntax, Operators, Tags, and Tests

Use this page when you need help writing expressions — delimiters, attribute access, nested rendering, control flow, fallback patterns, comparisons, logic operators, and type tests.

Pebble basics

Pebble templates use two primary delimiters:

  • {{ ... }} to output the result of an expression
  • {% ... %} to control template flow with tags such as if, for, or set

Examples:

{{ flow.id }}
{% if inputs.region == "eu" %}Europe{% endif %}

To escape Pebble syntax literally, use the raw tag described in Tags.

Accessing values

Use dot notation for standard property access:

{{ foo.bar }}

Use bracket notation for special characters or indexed access:

{{ foo['foo-bar'] }}
{{ items[0] }}

Parsing nested expressions

Kestra renders expressions once by default. If a variable contains Pebble that should be evaluated later, use render():

variables:
trigger_or_yesterday: "{{ trigger.date ?? (execution.startDate | dateAdd(-1, 'DAYS')) }}"
input_or_yesterday: "{{ inputs.mydate ?? (execution.startDate | dateAdd(-1, 'DAYS')) }}"
tasks:
- id: yesterday
type: io.kestra.plugin.core.log.Log
message: "{{ render(vars.trigger_or_yesterday) }}"
- id: input_or_yesterday
type: io.kestra.plugin.core.log.Log
message: "{{ render(vars.input_or_yesterday) }}"

This pattern is especially useful with namespace variables, composed flow variables, and fallback logic based on trigger context.

Multiline JSON bodies

When an HTTP request body contains multiline user input, avoid partial string interpolation. Instead, build the whole payload as a single Pebble expression so JSON escaping happens correctly.

id: multiline_input_passed_to_json_body
namespace: company.team
inputs:
- id: title
type: STRING
defaults: This is my title
- id: message
type: STRING
defaults: |-
This is my long
multiline message.
- id: priority
type: INT
defaults: 5
tasks:
- id: hello
type: io.kestra.plugin.core.http.Request
uri: https://kestra.io/api/mock
method: POST
body: |
{{ {
"title": inputs.title,
"message": inputs.message,
"priority": inputs.priority
} | toJson }}

Common syntax patterns

Comments

Use Pebble comments with {# ... #}:

{# This is a comment #}
{{ "Visible content" }}

In YAML, continue to use # for comments outside the expression itself.

Literals and collections

Pebble supports:

  • strings: "Hello World"
  • numbers such as 100 + 10l * 2.5
  • booleans: true, false
  • null: null
  • lists: ["apple", "banana"]
  • maps: {"apple":"red", "banana":"yellow"}

Named arguments

Filters, functions, and macros can accept named arguments:

{{ stringDate | date(existingFormat="yyyy-MMMM-d", format="yyyy/MMMM/d") }}

Control flow and fallbacks

Common patterns:

  • if and elseif for branching
  • for for iteration
  • ?? for fallback values
  • ? : for ternary expressions

Examples:

{{ inputs.mydate ?? (execution.startDate | dateAdd(-1, 'DAYS')) }}
{% for article in articles %}
{{ article.title }}
{% else %}
No articles available.
{% endfor %}

Inside a for loop, Pebble provides a loop object with properties such as loop.index, loop.first, loop.last, and loop.length. For the full table and examples, see for.

{% if category == "news" %}
{{ news }}
{% elseif category == "sports" %}
{{ sports }}
{% else %}
Select a category
{% endif %}

Operators

Comparisons

Supported comparison operators:

  • ==
  • !=
  • <
  • >
  • <=
  • >=
{% if execution.state == "SUCCESS" %}
Flow completed successfully.
{% endif %}
{% if taskrun.attemptsCount >= 3 %}
Max retries reached.
{% endif %}

Logic and boolean checks

Use:

  • and
  • or
  • not
  • is
  • contains

Use parentheses to group expressions and make precedence explicit:

{% if 2 is even and 3 is odd %}
...
{% endif %}
{% if (3 is not even) and (2 is odd or 3 is even) %}
...
{% endif %}

contains

Checks whether an item exists within a list, string, map, or array:

{% if ["apple", "pear", "banana"] contains "apple" %}
...
{% endif %}

For maps, contains checks for a matching key:

{% if {"apple": "red", "banana": "yellow"} contains "banana" %}
...
{% endif %}

To check for multiple items at once, pass a list on the right-hand side:

{% if ["apple", "pear", "banana", "peach"] contains ["apple", "peach"] %}
...
{% endif %}

contains also works inline in output expressions:

{{ inputs.mainString contains inputs.subString }}

isIn

Use isIn to test whether a value matches any item in a list. It reads more clearly than chaining multiple equality checks in runIf, SLAs, or alert conditions:

{{ execution.state isIn ['SUCCESS', 'KILLED', 'CANCELLED'] }}

Math and concatenation

Use:

  • +, -, *, /, %
  • ~ for string concatenation

Example:

{{ "apple" ~ "pear" ~ "banana" }}
{{ 2 + 2 / (10 % 3) * (8 - 1) }}

Fallbacks and conditionals

Use:

  • ?? for null-coalescing: returns the first non-null value
  • ??? for undefined-coalescing: returns the right-hand side only when the left is undefined (not just null)
  • ? : for ternary expressions

Examples:

{{ foo ?? bar ?? "default" }} {# first non-null value #}
{{ foo ??? "default" }} {# only if foo is undefined #}
{{ foo == null ? bar : baz }}
{{ foo ?? bar ?? raise }} {# raises an exception if all are undefined #}

For detailed null vs undefined behavior, see the Handling null and undefined values guide.

Operator precedence

Pebble operators are evaluated in this order:

  1. .
  2. |
  3. %, /, *
  4. -, +
  5. ==, !=, >, <, >=, <=
  6. is, is not
  7. and
  8. or

Tags

Pebble tags are enclosed in {% %} and control template flow.

set

Defines a variable in the template context:

{% set header = "Welcome Page" %}
{{ header }}
{# output: Welcome Page #}

if

Evaluates conditional logic. Use elseif and else for multiple branches:

{% if users is empty %}
No users available.
{% elseif users.length == 1 %}
One user found.
{% else %}
Multiple users found.
{% endif %}

for

Iterates over arrays, maps, or any java.lang.Iterable.

Iterating over a list:

{% for user in users %}
{{ user.name }} lives in {{ user.city }}.
{% else %}
No users found.
{% endfor %}

The else block runs when the collection is empty.

Iterating over a map:

{% for entry in map %}
{{ entry.key }}: {{ entry.value }}
{% endfor %}

Loop special variables:

Inside any for loop, Pebble provides a loop object with these properties:

VariableDescription
loop.indexZero-based index of the current iteration
loop.lengthTotal number of items in the iterable
loop.firsttrue on the first iteration
loop.lasttrue on the last iteration
loop.revindexNumber of iterations remaining

Example:

{% for user in users %}
{{ loop.index }}: {{ user.name }}{% if loop.last %} (last){% endif %}
{% endfor %}

filter

Applies a filter to a block of content. Filters can be chained:

{% filter upper %}
hello
{% endfilter %}
{# output: HELLO #}
{% filter lower | title %}
hello world
{% endfilter %}
{# output: Hello World #}

raw

Prevents Pebble from parsing its content — useful when you need to output literal {{ }} syntax:

{% raw %}{{ user.name }}{% endraw %}
{# output: {{ user.name }} #}

macro

Defines a reusable template snippet. Macros only have access to their own arguments by default:

{% macro input(type="text", name, value="") %}
type: "{{ type }}", name: "{{ name }}", value: "{{ value }}"
{% endmacro %}
{{ input(name="country") }}
{# output: type: "text", name: "country", value: "" #}

To access variables from the outer template context, pass _context explicitly:

{% set foo = "bar" %}
{% macro display(_context) %}
{{ _context.foo }}
{% endmacro %}
{{ display(_context) }}
{# output: bar #}

block

Defines a named, reusable template block. Use the block() function to render the block elsewhere:

{% block "header" %}
Introduction
{% endblock %}
{{ block("header") }}

Tests

Tests are used with is and is not to perform type and value checks.

defined

Checks whether a variable exists in the context (regardless of its value):

{% if missing is not defined %}
Variable is not defined.
{% endif %}

empty

Returns true when a variable is null, an empty string, an empty collection, or an empty map:

{% if user.email is empty %}
No email on record.
{% endif %}

null

Checks whether a variable is null:

{% if user.email is null %}
...
{% endif %}
{% if name is not null %}
...
{% endif %}

even and odd

Check whether an integer is even or odd:

{% if 2 is even %}
...
{% endif %}
{% if 3 is odd %}
...
{% endif %}

iterable

Returns true when a variable implements java.lang.Iterable. Use this to guard a for loop when the collection may not always be present:

{% if users is iterable %}
{% for user in users %}
{{ user.name }}
{% endfor %}
{% endif %}

json

Returns true when a variable is a valid JSON string:

{% if '{"test": 1}' is json %}
...
{% endif %}

map

Returns true when a variable is a map:

{% if {"apple": "red", "banana": "yellow"} is map %}
...
{% endif %}

Was this page helpful?