🚀 New! Kestra raises $3 million to grow Learn more

Syntax Reference

There are two primary delimiters used within a Pebble template: {{ ... }} and {% ... %}.

{{ ... }} is used to output the result of an expression. Expressions can be very simple (ex. a variable name) or much more complex.

{% ... %} is used to change the control flow of the template; it can contain an if-statement, define a parent template, define a new block, etc.

If needed, you can escape those delimiters and avoid Pebble templating applying on a variable thanks to the raw tag.


You can output variables directly inside an expression. For example, if the context contains a variable named foo which is a string with the value "bar", the following will output "bar":

{{ foo }}

You can use the dot (.) notation to access the attributes of variables. If the attribute contains a special character, you can use the subscript notation ([]) instead.

{{ foo.bar }} # Attribute 'bar' of 'foo'
{{ foo.bar.baz }} # Attribute 'baz' of attribute 'bar' of 'foo'
{{ foo['foo-bar'] }} # Attribute 'foo-bar' of 'foo'
{{ foo['foo-bar']['foo-baz'] }} # Attribute 'foo-baz' of attribute 'foo-bar' of 'foo'

Additionally, if foo is a List, then foo[idx] can be used to access the element of index idx of the foo variable.

If the value of a variable (or of an attribute) is undefined, it will throw an error and fail the task.


Expression output can be further modified with the use of filters. Filters are separated from the variable using a pipe symbol (|) and may have optional arguments in parentheses. Multiple filters can be chained, and the output of one filter is applied to the next.

{{ "If life gives you lemons, eat lemons." | upper | abbreviate(13) }}

The above example will output the following:



Whereas filters are intended to modify existing content/variables, functions are intended to generate new content. Similar to other programming languages, functions are invoked via their name followed by parentheses (()).

{{ max(user.score, highscore) }}

The above example will output the maximum of the two variables user.score and highscore.

Control Structure

Pebble provides several tags to control the flow of your template, two of the main ones being the for loop, and the if statement.

{% for article in articles %}
    <h3>{{ article.title }}</h3>
    <p>{{ article.content }}</p>
{% else %}
    <p> There are no articles. </p>
{% endfor %}
{% if category == "news" %}
    {{ news }}
{% elseif category == "sports" %}
    {{ sports }}
{% else %}
    <p>Please select a category</p>
{% endif %}


Macros are lightweight and re-usable template fragments. A macro is defined via the macro tag:

{% macro input(type, name) %}
    <input type="{{ type }}" name="{{ name }}" />
{% endmacro %}

And the macro will be invoked just like a function:

{{ input("text", "name", "Mitchell") }}

A macro does not have access to the main context; the only variables it can access are its local arguments.

Named Arguments

In filters, functions, or macros, you can use named arguments. Named arguments allow us to be more explicit on which arguments are passed and avoid mandating to pass default values.

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

Positional arguments can be used in conjunction with named arguments, but all positional arguments must come before any named arguments:

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

Macros are a great use case for named arguments because they also allow you to define default values for unused arguments:


{% macro input(type="text", name, value) %}
    <input type="{{ type }}" name="{{ name }}" value="{{ value }}" />
{% endmacro %}

{{ input(name="country") }}

{# will output: <input type="text" name="country" value="" /> #}


You add comments using the {# ... #} delimiters. These comments will not appear in the rendered output.

{% for article in articles %}
    <h3>{{ article.title }}</h3>
    <p>{{ article.content }}</p>
{% endfor %}


Expressions in a Pebble template are very similar to expressions found in Java.


The simplest form of expressions are literals. Literals are representations for Java types such as strings and numbers.

  • "Hello World": Everything between two double or single quotes is a string. You can use a backslash to escape quotation marks within the string.
  • "Hello #{who}": String interpolation is also possible using #{}. In this example, if the value of the variable who is "world", then the expression will be evaluated to "Hello world".
  • 100 + 10l * 2.5: Integers, longs and floating point numbers are similar to their Java counterparts.
  • true / false: Boolean values equivalent to their Java counterparts.
  • null: Represents no specific value, similar to its Java counterpart. none is an alias for null.


Both lists and maps can be created directly within the template.

  • ["apple", "banana", "pear"]: A list of strings
  • {"apple":"red", "banana":"yellow", "pear":"green"}: A map of strings

The collections can contain expressions.


Pebble allows you to calculate values using some basic mathematical operators. The following operators are supported:

  • +: Addition
  • -: Subtraction
  • /: Division
  • %: Modulus
  • *: Multiplication


You can combine multiple expressions with the following operators:

  • and: Returns true if both operands are true
  • or: Returns true if either operand is true
  • not: Negates an expression
  • (...): Groups expressions together


The following comparison operators are supported in any expression: ==, !=, <, >, >=, and <=.

{% if user.age >= 18 %}
{% endif %}


The is operator performs tests. Tests can be used to test an expression for certain qualities. The right operand is the name of the test:

{% if 3 is odd %}
{% endif %}

Tests can be negated by using the is not operator:

{% if name is not null %}
{% endif %}

Conditional (Ternary) Operator

The conditional operator is similar to its Java counterpart:

{{ foo ? "yes" : "no" }}

Null-Coalescing Operator

The null-coalescing operator allows to quickly test if a variable is defined (exists) and to use an alternative value if not:

{% set baz = "baz" %}
{{ foo ?? bar ?? baz }}

{# results in: 'baz' #}

{{ foo ?? bar ?? raise }}
{# results: an exception because none of the 3 vars is defined  #}

Operator Precedence

In order from highest to lowest precedence:

  • .
  • |
  • %, /, *
  • -, +
  • ==, !=, >, <, >=, <=
  • is, is not
  • and
  • or

Parents with Flowable Task.

When a task is in a hierarchy of Flowable Tasks, it can be complex to access the value of any of its parents because only the value of the direct parent task is accessible via taskrun.value.

To deal with this, we have included the parent and the parents variables.

This is illustrated in the following flow:

id: each-switch
namespace: io.kestra.tests

  - id: t1
    type: io.kestra.core.tasks.debugs.Return
    format: "{{task.id}} > {{taskrun.startDate}}"
  - id: 2_each
    type: io.kestra.core.tasks.flows.EachSequential
    value: ["a", "b"]
      # Switch
      - id: 2-1_switch
        type: io.kestra.core.tasks.flows.Switch
        value: "{{taskrun.value}}"
            - id: 2-1_switch-letter-a
              type: io.kestra.core.tasks.debugs.Return
              format: "{{task.id}}"
            - id: 2-1_switch-letter-b
              type: io.kestra.core.tasks.debugs.Return
              format: "{{task.id}}"

            - id: 2-1_each
              type: io.kestra.core.tasks.flows.EachSequential
              value: ["1", "2"]
              - id: 2-1-1_switch
                type: io.kestra.core.tasks.flows.Switch
                value: "{{taskrun.value}}"
                    - id: 2-1-1_switch-number-1
                      type: io.kestra.core.tasks.debugs.Return
                      format: "{{parents[0].taskrun.value}}"
                    - id: 2-1-1_switch-number-2
                      type: io.kestra.core.tasks.debugs.Return
                      format: "{{parents[0].taskrun.value}} {{parents[1].taskrun.value}}"
  - id: 2_end
    type: io.kestra.core.tasks.debugs.Return
    format: "{{task.id}} > {{taskrun.startDate}}"

As you can see, the parent variable gives direct access to the first parent, while the parents[INDEX] lets you access the parent deeper down the tree.

In the task 2-1-1_switch-number-2:

  • {{taskrun.value}}: mean the value of the task 2-1-1_switch
  • {{parents[0].taskrun.value}} or {{parent.taskrun.value}}: mean the value of the task 2-1_each
  • {{parents[1].taskrun.value}}: mean the value of the task 2-1_switch
  • {{parents[2].taskrun.value}}: mean the value of the task 2_each