From 60947e19a84740f0625a25267378af2050cbd06d Mon Sep 17 00:00:00 2001 From: Anna Geller Date: Wed, 20 Nov 2024 02:01:55 +0100 Subject: [PATCH] docs: move all expressions to a single reference doc (#1955) * docs: move all expressions to a single reference and improve the docs text * Update index.md * docs: move task run info * fix: wording + rm handlebars * Update index.md * Update index.md * Update index.md * Update index.md * avoid broken links * docs: avoid broken links --- .../docs/04.workflow-components/01.flow.md | 2 - .../01.tasks/02.taskruns.md | 116 +- .../04.workflow-components/03.execution.md | 1 - .../04.workflow-components/04.variables.md | 4 +- .../docs/04.workflow-components/06.outputs.md | 8 +- content/docs/05.concepts/04.secret.md | 2 +- content/docs/05.concepts/06.pebble.md | 2 - content/docs/05.concepts/11.storage.md | 10 +- .../0.14.0/recursive-rendering.md | 2 - .../docs/expressions/02.expression-types.md | 245 -- .../docs/expressions/02.expression-usage.md | 298 --- content/docs/expressions/03.filter/index.md | 31 - content/docs/expressions/03.filter/json.md | 114 - content/docs/expressions/03.filter/numeric.md | 74 - content/docs/expressions/03.filter/object.md | 196 -- content/docs/expressions/03.filter/string.md | 332 --- .../docs/expressions/03.filter/temporal.md | 180 -- content/docs/expressions/03.filter/yaml.md | 678 ------ content/docs/expressions/04.function.md | 571 ----- content/docs/expressions/05.operator.md | 161 -- content/docs/expressions/06.tag.md | 183 -- content/docs/expressions/07.test.md | 91 - .../expressions/08.deprecated-handlebars.md | 1010 -------- content/docs/expressions/index.md | 2119 ++++++++++++++++- nuxt.config.ts | 6 +- public/docs/expressions/printContext.png | Bin 0 -> 117951 bytes 26 files changed, 2215 insertions(+), 4221 deletions(-) delete mode 100644 content/docs/expressions/02.expression-types.md delete mode 100644 content/docs/expressions/02.expression-usage.md delete mode 100644 content/docs/expressions/03.filter/index.md delete mode 100644 content/docs/expressions/03.filter/json.md delete mode 100644 content/docs/expressions/03.filter/numeric.md delete mode 100644 content/docs/expressions/03.filter/object.md delete mode 100644 content/docs/expressions/03.filter/string.md delete mode 100644 content/docs/expressions/03.filter/temporal.md delete mode 100644 content/docs/expressions/03.filter/yaml.md delete mode 100644 content/docs/expressions/04.function.md delete mode 100644 content/docs/expressions/05.operator.md delete mode 100644 content/docs/expressions/06.tag.md delete mode 100644 content/docs/expressions/07.test.md delete mode 100644 content/docs/expressions/08.deprecated-handlebars.md create mode 100644 public/docs/expressions/printContext.png diff --git a/content/docs/04.workflow-components/01.flow.md b/content/docs/04.workflow-components/01.flow.md index c29e83974d..85590cc578 100644 --- a/content/docs/04.workflow-components/01.flow.md +++ b/content/docs/04.workflow-components/01.flow.md @@ -170,8 +170,6 @@ Flows have a number of variable expressions giving you information about them dy | `{{ flow.revision }}` | The revision of the flow. | -You can get the full list of variable expressions [here](../expressions/02.expression-types.md#flow-and-execution-expressions). - ## Listeners (deprecated) Listeners are special tasks that can listen to the current flow, and launch tasks *outside the flow*, meaning launch tasks that are not part of the flow. diff --git a/content/docs/04.workflow-components/01.tasks/02.taskruns.md b/content/docs/04.workflow-components/01.tasks/02.taskruns.md index 1049496352..5e44690551 100644 --- a/content/docs/04.workflow-components/01.tasks/02.taskruns.md +++ b/content/docs/04.workflow-components/01.tasks/02.taskruns.md @@ -49,7 +49,7 @@ namespace: company.team tasks: - id: return - type: io.kestra.plugin.core.debug.Return + type: io.kestra.plugin.core.debug.Return format: "{{ taskrun }}" ``` @@ -62,6 +62,8 @@ The logs show the following: } ``` +## Task Run Values + Some [Flowable Tasks](./00.flowable-tasks.md), such as [ForEach](./00.flowable-tasks.md) and [ForEachItem](./00.flowable-tasks.md#foreachitem), group tasks together. You can use the expression `{{ taskrun.value }}` to access the value for that task run. In the example below, `foreach` will iterate twice over the values `[1, 2]`: @@ -77,11 +79,19 @@ tasks: tasks: - id: log type: io.kestra.plugin.core.log.Log - message: "{{ taskrun.value }}" + message: + - "{{ taskrun }}" + - "{{ taskrun.value }}" + - "{{ taskrun.id }}" + - "{{ taskrun.startDate }}" + - "{{ taskrun.attemptsCount }}" + - "{{ taskrun.parentId }}" ``` This outputs two separate log tasks, one with `1` and the other with `2`. -You can also use the `{{ parents }}` expression to access a task run value from a parent task. Here's an example of it with `ForEach`: +### Parent Task Run Values + +You can also use the `{{ parent.taskrun.value }}` expression to access a task run value from a parent task within nested flowable child tasks: ```yaml id: loop @@ -101,22 +111,75 @@ tasks: then: - id: log_parent type: io.kestra.plugin.core.log.Log - message: "{{ parents }}" + message: "{{ parent.taskrun.value }}" ``` -This will iterate through the `log` and `if` tasks twice as there are two items in `values` property. -The `log_parent` task outputs the task run value produced by `foreach` on the first iteration: +This will iterate through the `log` and `if` tasks twice as there are two items in `values` property. The `log_parent` task will log the parent task run value as `1` and then `2`. -```json -[ - { - "taskrun": { - "value": "1" - } - } -] +### Parent vs. Parents in Nested Flowable Tasks + +When using nested [Flowable tasks](./00.flowable-tasks.md), only the direct parent task is accessible via `taskrun.value`. To access a parent task higher up the tree, you can use the `parent` and the `parents` expressions. + +The following flow shows a more complex example with nested flowable parent tasks: + +```yaml +id: each_switch +namespace: company.team + +tasks: + - id: simple + type: io.kestra.plugin.core.log.Log + message: + - "{{ task.id }}" + - "{{ taskrun.startDate }}" + + - id: hierarchy_1 + type: io.kestra.plugin.core.flow.ForEach + values: ["caseA", "caseB"] + tasks: + - id: hierarchy_2 + type: io.kestra.plugin.core.flow.Switch + value: "{{ taskrun.value }}" + cases: + caseA: + - id: hierarchy_2_a + type: io.kestra.plugin.core.debug.Return + format: "{{ task.id }}" + caseB: + - id: hierarchy_2_b_first + type: io.kestra.plugin.core.debug.Return + format: "{{ task.id }}" + + - id: hierarchy_2_b_second + type: io.kestra.plugin.core.flow.ForEach + values: ["case1", "case2"] + tasks: + - id: switch + type: io.kestra.plugin.core.flow.Switch + value: "{{ taskrun.value }}" + cases: + case1: + - id: switch_1 + type: io.kestra.plugin.core.log.Log + message: + - "{{ parents[0].taskrun.value }}" + - "{{ parents[1].taskrun.value }}" + case2: + - id: switch_2 + type: io.kestra.plugin.core.log.Log + message: + - "{{ parents[0].taskrun.value }}" + - "{{ parents[1].taskrun.value }}" + - id: simple_again + type: io.kestra.plugin.core.log.Log + message: + - "{{ task.id }}" + - "{{ taskrun.startDate }}" ``` +The `parent` variable gives direct access to the first parent, while the `parents[INDEX]` gives you access to the parent higher up the tree. + + ::collapse{title="Task Run JSON Object Example"} ```json { @@ -152,51 +215,50 @@ The `log_parent` task outputs the task run value produced by `foreach` on the fi "histories": [ { "state": "CREATED", - "date": "2021-05-04T12:02:54.121836Z" + "date": "2025-05-04T12:02:54.121836Z" }, { "state": "RUNNING", - "date": "2021-05-04T12:02:54.121841Z" + "date": "2025-05-04T12:02:54.121841Z" }, { "state": "SUCCESS", - "date": "2021-05-04T12:02:54.131892Z" + "date": "2025-05-04T12:02:54.131892Z" } ], "duration": "PT0.010056S", - "endDate": "2021-05-04T12:02:54.131892Z", - "startDate": "2021-05-04T12:02:54.121836Z" + "endDate": "2025-05-04T12:02:54.131892Z", + "startDate": "2025-05-04T12:02:54.121836Z" } } ], "outputs": { - "value": "1-1_return > s1 ⬅ 2021-05-04T12:02:53.938333Z" + "value": "1-1_return > s1 ⬅ 2025-05-04T12:02:53.938333Z" }, "state": { "current": "SUCCESS", "histories": [ { "state": "CREATED", - "date": "2021-05-04T12:02:53.938333Z" + "date": "2025-05-04T12:02:53.938333Z" }, { "state": "RUNNING", - "date": "2021-05-04T12:02:54.116336Z" + "date": "2025-05-04T12:02:54.116336Z" }, { "state": "SUCCESS", - "date": "2021-05-04T12:02:54.144135Z" + "date": "2025-05-04T12:02:54.144135Z" } ], "duration": "PT0.205802S", - "endDate": "2021-05-04T12:02:54.144135Z", - "startDate": "2021-05-04T12:02:53.938333Z" + "endDate": "2025-05-04T12:02:54.144135Z", + "startDate": "2025-05-04T12:02:53.938333Z" } } ``` :: -Read more about it on the [Expression Usage](../../expressions/02.expression-usage.md#parent-tasks-with-flowable-tasks) page. ## Task Runs Page (EE) @@ -208,4 +270,4 @@ If you have Kestra setup using the [Kafka and Elasticsearch backend](../../07.ar ![taskrun_view](/docs/workflow-components/taskrun_view.png) -It's similar to the [Execution View](../../08.ui/02.executions.md) but only shows Task Runs. +It's similar to the [Execution View](../../08.ui/02.executions.md) but only shows Task Runs. diff --git a/content/docs/04.workflow-components/03.execution.md b/content/docs/04.workflow-components/03.execution.md index cd8644bda7..17426cf1e9 100644 --- a/content/docs/04.workflow-components/03.execution.md +++ b/content/docs/04.workflow-components/03.execution.md @@ -109,7 +109,6 @@ There are a number of execution expressions which you can use inside of your flo | `{{ execution.startDate }}` | The start date of the current execution, can be formatted with `{{ execution.startDate \| date("yyyy-MM-dd HH:mm:ss.SSSSSS") }}`. | | `{{ execution.originalId }}` | The original execution ID, this id will never change even in case of replay and keep the first execution ID. | -You can read more about them on the [expressions page](../expressions/02.expression-types.md). ## Execute a flow from the UI diff --git a/content/docs/04.workflow-components/04.variables.md b/content/docs/04.workflow-components/04.variables.md index fbfe722dc5..14e593115f 100644 --- a/content/docs/04.workflow-components/04.variables.md +++ b/content/docs/04.workflow-components/04.variables.md @@ -43,7 +43,7 @@ Since 0.14, Variables are no longer rendered recursively. You can read more abou ## Dynamic Variables -If you want to have an expression inside of your variable, you will need to wrap it in `render` when you use it in a task. +If you want to have an expression inside of your variable, you will need to wrap it in `render` when you use it in a task. For example, this variable will only display the current time in the log message when wrapped in `render`. Otherwise, the log message will just contain the expression as a string: @@ -68,7 +68,7 @@ You will need to wrap the variable expression in `render` every time you want to ### How do I escape a block in Pebble syntax to ensure that it won't be parsed? -To ensure that a block of code won't be parsed by Pebble, you can use the `{% raw %}` and `{% endraw %}` [Pebble tags](../expressions/06.tag.md#raw). For example, the following Pebble expression will return the string `{{ myvar }}` instead of the value of the `myvar` variable: +To ensure that a block of code won't be parsed by Pebble, you can use the `{% raw %}` and `{% endraw %}` [Pebble tags](../expressions/index.md#tag). For example, the following Pebble expression will return the string `{{ myvar }}` instead of the value of the `myvar` variable: ```yaml {% raw %}{{ myvar }}{% endraw %} diff --git a/content/docs/04.workflow-components/06.outputs.md b/content/docs/04.workflow-components/06.outputs.md index 0bc76b8da4..88d40321cf 100644 --- a/content/docs/04.workflow-components/06.outputs.md +++ b/content/docs/04.workflow-components/06.outputs.md @@ -45,7 +45,7 @@ tasks: In the example above, the first task produces an output based on the task property `format`. This output attribute is then used in the second task `message` property. -The expression `{{ outputs.produce_output.value }}` references the previous task output attribute. You can read more about the expression syntax on the [Using Expressions](../expressions/02.expression-usage.md) page. +The expression `{{ outputs.produce_output.value }}` references the previous task output attribute. ::alert{type="info"} In the example above, the **Return** task produces an output attribute `value`. Every task produces different output attributes. You can look at each task outputs documentation or use the **Outputs** tab of the **Executions** page to find out about specific task output attributes. @@ -133,7 +133,7 @@ The **Outputs** tab would contain the output for each of the inner task. ### Loop over a list of JSON objects -On loop, the `value` is always a JSON string, so the `{{ taskrun.value }}` is the current element as JSON string. If you want to access properties, you need to use the [json function](../expressions/04.function.md#fromjson) to have a proper object and to access each property easily. +Within the loop, the `value` is always a JSON string, so the `{{ taskrun.value }}` is the current element as JSON string. To access properties, you need to wrap it in the `fromJson()` function to have a JSON object allowing to access each property easily. ```yaml id: loop_sequentially_over_list @@ -148,7 +148,7 @@ tasks: tasks: - id: inner type: io.kestra.plugin.core.debug.Return - format: "{{ json(taskrun.value).key }} > {{ json(taskrun.value).value }}" + format: "{{ fromJson(taskrun.value).key }} > {{ fromJson(taskrun.value).value }}" ``` @@ -238,7 +238,7 @@ tasks: When there are multiple levels of [EachSequential](/plugins/core/tasks/flows/io.kestra.plugin.core.flow.EachSequential) tasks, you can use the `parents` variable to access the `taskrun.value` of the parent of the current EachSequential. For example, for two levels of EachSequential you can use `outputs.sibling[parents[0].taskrun.value][taskrun.value].value`. -The latter can become very complex when parents exist (multiple imbricated EachSequential). For this, you can use the special [currentEachOutput](../expressions/04.function.md#currenteachoutput) function. No matter the number of parents, the following example will retrieve the correct output attribute: `currentEachOutput(outputs.sibling).value` thanks to this function. +The latter can become very complex when parents exist (multiple imbricated EachSequential). For this, you can use the `currentEachOutput()` function. No matter the number of parents, the following example will retrieve the correct output attribute: `currentEachOutput(outputs.sibling).value` thanks to this function. ::alert{type="warning"} Accessing sibling task outputs is impossible on [Parallel](/plugins/core/tasks/flows/io.kestra.plugin.core.flow.Parallel) or [EachParallel](/plugins/core/tasks/flows/io.kestra.plugin.core.flow.EachParallel) as they run tasks in parallel. diff --git a/content/docs/05.concepts/04.secret.md b/content/docs/05.concepts/04.secret.md index 93c82e4b3b..be954fbf0a 100644 --- a/content/docs/05.concepts/04.secret.md +++ b/content/docs/05.concepts/04.secret.md @@ -17,7 +17,7 @@ To retrieve secrets in a flow, use the `secret()` function, e.g. `"{{ secret('AP Your flows often need to interact with external systems. To do that, they need to programmatically authenticate using passwords or API keys. Secrets help you securely store such variables and avoid hard-coding sensitive information within your workflow code. -You can leverage the `secret()` function, described in the [function section](../expressions/04.function.md#secret), to retrieve sensitive variables within your flow code. +You can leverage the `secret()` function to retrieve sensitive variables within your flow code. ## Secrets in the Enterprise Edition diff --git a/content/docs/05.concepts/06.pebble.md b/content/docs/05.concepts/06.pebble.md index c11d6a4a95..42c5bfe5eb 100644 --- a/content/docs/05.concepts/06.pebble.md +++ b/content/docs/05.concepts/06.pebble.md @@ -107,8 +107,6 @@ Pebble can be very useful to make small transformation on the fly - without the For instance, we can use the `date` filter to format date values: `'{{ inputs.my_date | date("yyyyMMdd") }}'` -You can find more documentation on the `date` filter on the [Expressions page](../expressions/03.filter/temporal.md#date) - ## Coalesce operator to conditionally use trigger or execution date Most of the time, a flow will be triggered automatically. Either on schedule or based on external events. It’s common to use the date of the execution to process the corresponding data and make the flow dependent on time. diff --git a/content/docs/05.concepts/11.storage.md b/content/docs/05.concepts/11.storage.md index d84ced535f..a0118149bb 100644 --- a/content/docs/05.concepts/11.storage.md +++ b/content/docs/05.concepts/11.storage.md @@ -73,7 +73,7 @@ tasks: to: "/upload/file.ion" ``` -If you need to access data from the internal storage, you can use the Pebble [read](../expressions/04.function.md#read) function to read the file's content as a string. +If you need to access data from the internal storage, you can use the `read()` function to read the file's content as a string. Dedicated tasks allow managing the files stored inside the internal storage: - [Concat](/plugins/core/tasks/storages/io.kestra.plugin.core.storage.Concat): concat multiple files. @@ -161,9 +161,9 @@ As we can see, when we `Set` a new value for `user_name`, we have to use another ## Processing data -You can make basic data processing thanks to variables processing offered by the Pebble templating engine, see [variables basic usage](../expressions/index.md). +For basic data processing, you can leverage Kestra's [Pebble templating engine](../expressions/index.md). -But these are limited, and you may need more powerful data processing tools; for this, Kestra offers various data processing tasks like file transformations or scripts. +For more complex data transformations, Kestra offers various data processing plugins incl. transform tasks or custom scripts. ### Converting files @@ -440,9 +440,9 @@ The same syntax applies to SQL queries, custom scripts, and many more. Check the #### How to read a file from the internal storage as a JSON object? -There is a [Pebble function](../expressions/04.function.md#fromjson) called `{{ json(myvar) }}` and a [Pebble transformation filter](../expressions/03.filter/json.md) that you can apply using `{{ myvar | json }}`. +You can use the Pebble function `{{ fromJson(myvar) }}` and a `{{ myvar | toJson }}` filter to process JSON data. -::collapse{title="The json() function"} +::collapse{title="The fromJson() function"} The function is used to convert a string to a JSON object. For example, the following Pebble expression will convert the string `{"foo": [666, 1, 2]}` to a JSON object and then return the first value of the `foo` key, which is `42`: diff --git a/content/docs/11.migration-guide/0.14.0/recursive-rendering.md b/content/docs/11.migration-guide/0.14.0/recursive-rendering.md index 0ee6483cff..e32d32fb26 100644 --- a/content/docs/11.migration-guide/0.14.0/recursive-rendering.md +++ b/content/docs/11.migration-guide/0.14.0/recursive-rendering.md @@ -40,8 +40,6 @@ triggers: cron: "*/1 * * * *" ``` -Check the [`render()` function's documentation](../../expressions/04.function.md#render) for more details about how to use that function and when to use it. - --- ## Migrating a 0.13.0 flow to the new rendering behavior in 0.14.0 diff --git a/content/docs/expressions/02.expression-types.md b/content/docs/expressions/02.expression-types.md deleted file mode 100644 index bdfb89ab65..0000000000 --- a/content/docs/expressions/02.expression-types.md +++ /dev/null @@ -1,245 +0,0 @@ ---- -title: Expression Types -icon: /docs/icons/expression.svg ---- - -There are many ways to use expressions in Kestra. This page will guide you through different types of expressions. - -## Flow and Execution expressions - -Flow and Execution expressions allow using the current execution context to set task properties. For example: name a file with the current date or the current execution id. - -The following table lists all the default expressions available on each execution. - -| Parameter | Description | -|-------------------------------|-----------------------------------------------------------------------------------------------------------------------------------| -| `{{ flow.id }}` | The identifier of the flow. | -| `{{ flow.namespace }}` | The name of the flow namespace. | -| `{{ flow.tenantId }}` | The identifier of the tenant (EE only). | -| `{{ flow.revision }}` | The revision of the flow. | -| `{{ execution.id }}` | The execution ID, a generated unique id for each execution. | -| `{{ execution.startDate }}` | The start date of the current execution, can be formatted with `{{ execution.startDate \| date("yyyy-MM-dd HH:mm:ss.SSSSSS") }}`. | -| `{{ execution.originalId }}` | The original execution ID, this id will never change even in case of replay and keep the first execution ID. | -| `{{ task.id }}` | The current task ID | -| `{{ task.type }}` | The current task Type (Java fully qualified class name). | -| `{{ taskrun.id }}` | The current task run ID. | -| `{{ taskrun.startDate }}` | The current task run start date. | -| `{{ taskrun.parentId }}` | The current task run parent identifier. Only available with tasks inside a Flowable Task. | -| `{{ taskrun.value }}` | The value of the current task run, only available with tasks wrapped in Flowable Tasks. | -| `{{ taskrun.attemptsCount }}` | The number of attempts for the current task (when retry or restart is performed). | -| `{{ parent.taskrun.value }}` | The value of the closest (first) parent task run, only available with tasks inside a Flowable Task. | -| `{{ parent.outputs }}` | The outputs of the closest (first) parent task run Flowable Task, only available with tasks wrapped in a Flowable Task. | -| `{{ parents }}` | The list of parent tasks, only available with tasks wrapped in a Flowable Task. | -| `{{ labels }}` | The executions labels accessible by keys, for example: `{{ labels.myKey1 }}` . | - -If execution is created from a Schedule-type trigger, these expressions are also available: - -| Parameter | Description | -| ---------- | ----------- | -| `{{ trigger.date }}` | The date of the current schedule. | -| `{{ trigger.next }}` | The date of the next schedule. | -| `{{ trigger.previous }}` | The date of the previous schedule. | - -If execution is created from a Flow-type trigger, these expressions are also available: - -| Parameter | Description | -| ---------- | ----------- | -| `{{ trigger.executionId }}` | The ID of the execution that triggers the current flow. | -| `{{ trigger.namespace }}` | The namespace of the flow that triggers the current flow. | -| `{{ trigger.flowId }}` | The ID of the flow that triggers the current flow. | -| `{{ trigger.flowRevision }}` | The revision of the flow that triggers the current flow. | - -All these expressions can be accessed using the Pebble template syntax `{{expression}}`: - -```yaml -id: expressions -namespace: company.team - -tasks: - - id: echo - type: io.kestra.plugin.core.debug.Return - format: | - taskid: {{ task.id }} - date: {{ execution.startDate | date("yyyy-MM-dd HH:mm:ss.SSSSSS") }} -``` - -::alert{type="info"} -`{{ execution.startDate | date("yyyy-MM-dd HH:mm:ss.SSSSSS") }}` uses the `date` filter to format the `execution.startDate` variable with the date pattern `yyyy-MM-dd HH:mm:ss.SSSSSS`. -:: - -## Environment variables - -By default, Kestra allows access to environment variables that start with `KESTRA_` unless configured otherwise, see how you can configure environment variables in the `variables` configuration in your Kestra server settings. - -To access an environment variable `KESTRA_FOO` from one of your tasks, you can use `{{ envs.foo }}`, the variable's name is the part after the `KESTRA_` prefix in **lowercase**. - -## Global variables - -You can define global variables inside Kestra's configuration files and access them using `{{ globals.foo }}`. - -## Flow variables - -You can declare variables at the flow level with the `variables` property, then refer to these variables using the `vars.my_variable` syntax, for example: - -```yaml -id: flow_variables -namespace: company.team - -variables: - my_variable: "my_value" - -tasks: - - - id: print_variable - type: io.kestra.plugin.core.debug.Return - format: "{{ vars.my_variable }}" -``` - -## Inputs - -You can use any flow inputs using `inputs.inputName`, for example: - -```yaml -id: render_inputs -namespace: company.team - -inputs: - - id: myInput - type: STRING - -tasks: - - id: myTask - type: io.kestra.plugin.core.debug.Return - format: "{{ inputs.myInput }}" -``` - -## Secrets - -You can retrieve secrets in your flow using the `secret()` function. Here is an example: - -```yaml -id: use_secret_in_flow -namespace: company.team - -tasks: - - id: myTask - type: io.kestra.plugin.core.debug.Return - format: "{{ secret('MY_SECRET') }}" -``` - -Secrets can be provided on both open-source and [Enterprise Edition](/enterprise). Check the [Secrets](../05.concepts/04.secret.md) documentation for more details. - -## Namespace variables (EE) - -Namespace variables are key-value pairs defined in a YAML configurtion. They can be nested and used in your flows using the dot notation e.g. `{{ namespace.myproject.myvariable }}`. You can define namespace variables in the `Variables` tab in the UI. - -::alert{type="warning"} -Namespace variables is an [Enterprise Edition](/enterprise) feature. -:: - -Namespace variables are scoped to the specific namespace and are inherited by child namespaces. Your flow then refers to these variables using the `namespace.your_variable` syntax, for example: - -```yaml -id: namespace_variables -namespace: company.team - -tasks: - - id: myTask - type: io.kestra.plugin.core.debug.Return - format: "{{ namespace.your_variable }}" -``` - -However, note that if your namespace variable contains Pebble expressions like e.g. `{{ secret('GITHUB_TOKEN') }}`, you must use the `render` function to render the variable. Assuming the following code being added to the Variables tab in a Namespace UI: - -```yaml -github: - token: "{{ secret('GITHUB_TOKEN') }}" -``` - -To reference the `github.token` variable in your flow, you must use the `render` function: - -```yaml -id: recursive_namespace_variables_rendering -namespace: company.team -tasks: - - id: myTask - type: io.kestra.plugin.core.debug.Return - format: "{{ render(namespace.github.token) }}" -``` - -The `render()` function is required to parse Namespace or Flow variables that contain Pebble expressions, as this function allows for recursive rendering. If you don't use the `render` function, the variable will be rendered as a string, and the Pebble expressions within the variable will not be evaluated. - -## Outputs - -You can use any task output attributes using `"{{ outputs.taskId.outputAttribute }}"` where: - -- the `taskId` is the ID of the task. -- the `outputAttribute` is the attribute of the task output you want to use; each task can emit various output attributes — check the task documentation for the list of output attributes for any given task. - -Example of a flow using `outputs` to pass data between tasks: - -```yaml -id: pass_data_between_tasks -namespace: company.team - -tasks: - - id: first - type: io.kestra.plugin.core.debug.Return - format: First output value - - - id: - type: io.kestra.plugin.core.debug.Return - format: Second output value - - - id: print_both_outputs - type: io.kestra.plugin.core.log.Log - message: | - First: {{ outputs.first.value }} - Second: {{ outputs['second-task'].value }} -``` - -::alert{type="info"} -The `Return`-type task has an output attribute `value` which is used by the `print_both_outputs` task. -The `print_both_outputs` task demonstrates two ways to access task outputs: -1. The most common is the dot notation `{{ outputs.first.value }}` -2. The subscript notation `{{ outputs['second-example'].value }}` using the square brackets is needed when your task ID contains special characters. such as hyphens. -:: - -## Pebble templating: example - -The example below will parse the Pebble expressions within the `variables` based on the `inputs` and `trigger` values. Both variables use the [Null-Coalescing Operator](./02.expression-usage.md#null-coalescing-operator) to use the first non-null value. - -Here, the first variable `trigger_or_yesterday` will evaluate to a `trigger.date` if the flow runs on schedule. Otherwise, it will evaluate to the yesterday's date by using the `execution.startDate` minus one day. - -The second variable `input_or_yesterday` will evaluate to the `mydate` input if it's provided. Otherwise, it will evaluate to the yesterday's date — again, using the `execution.startDate` and subtracting one day with the help of the `dateAdd` function. - -```yaml -id: render_complex_expressions -namespace: company.team - -inputs: - - id: mydate - type: DATETIME - required: false - -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) }}" -``` - -Note how we use the `render` function to render the variables. This function is required when you want to render a variable that contains Pebble expressions, allowing for recursive rendering. If you don't use the `render` function, the variable will be rendered as a string, and the Pebble expressions within the variable will not be evaluated. - -## Pebble templating: deep dive - -Pebble templating offers a myriad of ways to parse expressions. - - diff --git a/content/docs/expressions/02.expression-usage.md b/content/docs/expressions/02.expression-usage.md deleted file mode 100644 index f9a7a9bd26..0000000000 --- a/content/docs/expressions/02.expression-usage.md +++ /dev/null @@ -1,298 +0,0 @@ ---- -title: Expression Usage -icon: /docs/icons/expression.svg ---- - -This page summarizes the main syntax of filters, functions, and control structures available in Pebble templating. - -## 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 use the dot (`.`) notation to access child attributes. If the attribute contains a special character, you can use the subscript notation (`[]`) instead. - -```twig -{{ 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' -``` - -::alert{type="warning"} -When using task, variable or output names containing a hyphen symbol `-`, make sure to use the subscript notation (`[]`) e.g. `"{{ outputs.mytask.myoutput['foo-bar'] }}"` because `-` is a special character in the Pebble templating engine. -:: - -If `foo` would be a List, then `foo[idx]` can be used to access the elements of index `idx` of the foo variable. - -## Filters - -You can use filters in your expressions using a pipe symbol (`|`). Multiple filters can be chained, and the output of one filter is applied to the next one. - -```twig -{{ "When life gives you lemons, make lemonade." | upper | abbreviate(13) }} -``` -The above example will output the following: - -```twig -WHEN LIFE ... -``` - -## Functions - -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 `function_name()`. - -```twig -{{ 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](./06.tag.md#for) loop, and the [if](./06.tag.md#if) statement. - -```twig -{% for article in articles %} - {{ article.title }} -{% else %} - "There are no articles." -{% endfor %} -``` - -```twig -{% if category == "news" %} - {{ news }} -{% elseif category == "sports" %} - {{ sports }} -{% else %} - "Please select a category" -{% endif %} -``` - -## Macros - -Macros are lightweight and re-usable template fragments. A macro is defined via the [macro](./06.tag.md#macro) tag: - -```twig -{% macro input(type, name) %} - {{ name }} is of type {{ type }} -{% endmacro %} -``` - -And the macro will be invoked just like a function: - -```twig -{{ input("text", "Mitchell") }} -``` - -A macro does not have access to the main context; the only variables it can access are its local arguments. - -Here is an example flow showing the usage of macro: - -```yaml -id: macro-example -namespace: company.team - -tasks: - - id: log_output - type: io.kestra.plugin.core.log.Log - message: | - {% macro input(type, name) %}{{ name }} is of type {{ type }}{% endmacro %} - {{ input("text", "John") }} - {{ input("number", 1) }} -``` - -## 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. - -```twig -{{ 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: - -```twig -{{ 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: - -```twig - -{% macro input(type="text", name, value) %} - type is "{{ type }}", name is "{{ name }}", and value is "{{ value }}" -{% endmacro %} - -{{ input(name="country") }} - -{# will output: type is "text", name is "country", and value is "" #} - -``` - -## Comments - -You add comments using the `{# ... #}` delimiters. These comments will not appear in the rendered output. - -```twig -{# THIS IS A COMMENT #} -{% for article in articles %} - {{ article.title }} has content {{ article.content }} -{% endfor %} -``` - -## Literals - -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. - -## Collections - -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. - -## Math - -Pebble allows you to calculate values using some basic mathematical operators. The following operators are supported: -- `+`: Addition -- `-`: Subtraction -- `/`: Division -- `%`: Modulus -- `*`: Multiplication - -## Logic - -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 - -## Comparisons - -The following comparison operators are supported in any expression: `==`, `!=`, `<`, `>`, `>=`, and `<=`. - -```twig -{% if user.age >= 18 %} - ... -{% endif %} -``` - -## Tests - -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: - -```twig -{% if 3 is odd %} - ... -{% endif %} -``` - -Tests can be negated by using the is not operator: - -```twig -{% if name is not null %} - ... -{% endif %} -``` - - -## Conditional (Ternary) Operator -The conditional operator is similar to its Java counterpart: - -```twig -{{ 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: - -```twig -{% 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` - -## Parent tasks with Flowable tasks - -When using nested Flowable Tasks, only the direct parent task is accessible via `taskrun.value`. To access a parent task higher up the tree, you can use the `parent` and the `parents` expressions. - -The following flow shows how to access the parent tasks: - -```yaml -id: each_switch -namespace: company.team - -tasks: - - id: simple - type: io.kestra.plugin.core.debug.Return - format: "{{ task.id }} > {{ taskrun.startDate }}" - - - id: hierarchy_1 - type: io.kestra.plugin.core.flow.EachSequential - value: ["a", "b"] - tasks: - - id: hierarchy_2 - type: io.kestra.plugin.core.flow.Switch - value: "{{ taskrun.value }}" - cases: - a: - - id: hierarchy_2_a - type: io.kestra.plugin.core.debug.Return - format: "{{ task.id }}" - b: - - id: hierarchy_2_b - type: io.kestra.plugin.core.debug.Return - format: "{{ task.id }}" - - - id: hierarchy_2_b_second - type: io.kestra.plugin.core.flow.EachSequential - value: ["1", "2"] - tasks: - - id: switch - type: io.kestra.plugin.core.flow.Switch - value: "{{ taskrun.value }}" - cases: - 1: - - id: switch_1 - type: io.kestra.plugin.core.debug.Return - format: "{{ parents[0].taskrun.value }}" - 2: - - id: switch_2 - type: io.kestra.plugin.core.debug.Return - format: "{{ parents[0].taskrun.value}} {{ parents[1].taskrun.value }}" - - id: simple_again - type: io.kestra.plugin.core.debug.Return - format: "{{ task.id }} > {{ taskrun.startDate }}" -``` - -The `parent` variable gives direct access to the first parent, while the `parents[INDEX]` gives you access to the parent higher up the tree. diff --git a/content/docs/expressions/03.filter/index.md b/content/docs/expressions/03.filter/index.md deleted file mode 100644 index 06632e9dc6..0000000000 --- a/content/docs/expressions/03.filter/index.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: Filter -icon: /docs/icons/expression.svg ---- - -Filters can be used in expressions to perform some transformations on variables such as formatting a date, converting a string to uppercase, or joining a list of strings. - -Filters are separated from the variable name by a pipe symbol `|`. Multiple filters can be chained together so that the output of one filter is applied to the next one. - -The following example turns the name into title case: -```twig -{{ name | title }} -``` - -Filters that accept arguments have parentheses around the arguments. This example joins the elements of a list into a string, with list elements separated by commas: -```twig -{{ list | join(', ') }} -``` - -To apply a filter on a section of code, wrap it with the `filter` tag: - -```twig -{% filter lower | title %} - hello -{% endfilter %} -``` - -Each section below represents a built-in filter type. - -::ChildCard -:: diff --git a/content/docs/expressions/03.filter/json.md b/content/docs/expressions/03.filter/json.md deleted file mode 100644 index 0b255dff30..0000000000 --- a/content/docs/expressions/03.filter/json.md +++ /dev/null @@ -1,114 +0,0 @@ ---- -title: JSON Filters -icon: /docs/icons/expression.svg ---- - -JSON filters are used to manipulate JSON objects, often API responses. - -## toJson - -The `toJson` filter will convert any object to a JSON string. - -The following expression `{{ [1, 2, 3] | toJson }}` will result in the JSON string `'[1, 2, 3]'`. - -Similarly: -- `{{ true | toJson }}` will result in `'true'` -- `{{ "foo" | toJson }}` will result in `'"foo"'` - -::alert{type="info"} -If you were using Kestra in a version prior to [v0.18.0](/blogs/2024-08-06-release-0-18.md), this filter used to be named `json`. We've renamed it to `toJson` for more clarity. The renaming has been implemented in a non-breaking way — using `json` will raise a warning in the UI but it will still work. -:: - - -## jq - -The `jq` filter apply a [JQ expression](https://stedolan.github.io/jq/) to a variables. The filter always return an array of result and will be formatted as json. You can use the filter [first](./object.md#first) in order to return the first (and potentially the only) result of the jq filter. - -```twig -{{ [1, 2, 3] | jq('.') }} -{# results in: '[1, 2, 3]' #} - -{{ [1, 2, 3] | jq('.[0]') | first }} -{# results in: '1' #} -``` - - -Another example, if the current context is: -```json -{ - "outputs": { - "task1": { - "value": 1, - "text": "awesome1" - }, - "task2": { - "value": 2, - "text": "awesome2" - } - } -} -``` - -```twig -{{ outputs | jq('.task1.value') | first }} -``` - -the output will be `1`. - - -**Arguments** -- `expression`: the jq expression to apply - -## Manipulating JSON payload - -Here is a detailed example having multiple JSON manipulations. In this example, we take a JSON payload as input, and perform multiple manipulations on it to derive different outputs. - -```yaml -id: myflow -namespace: company.myteam - -inputs: - - id: payload - type: JSON - defaults: |- - { - "name": "John Doe", - "score": { - "English": 72, - "Maths": 88, - "French": 95, - "Spanish": 85, - "Science": 91 - }, - "address": { - "city": "Paris", - "country": "France" - }, - "graduation_years": [2020, 2021, 2022, 2023] - } - -tasks: - - id: print_status - type: io.kestra.plugin.core.log.Log - message: - - "Student name: {{ inputs.payload.name }}" # Extracting a value from a JSON payload - - "Score in languages: {{ inputs.payload.score.English + inputs.payload.score.French + inputs.payload.score.Spanish }}" # Extracting the numbers from JSON payload, and suming them up - - "Total subjects: {{ inputs.payload.score | length }}" # Counting the length of map - - "Total score: {{ inputs.payload.score | values | jq('reduce .[] as $num (0; .+$num)') | first }}" # logic to get all the values in the `score` map and add them to get the total score - - "Complete address: {{ inputs.payload.address.city }}, {{ inputs.payload.address.country | upper }}" # String concatenation, and conversion - - "Total years for graduation: {{ inputs.payload.graduation_years | length }}" # Counting the length of array - - "Started college in: {{ inputs.payload.graduation_years | first }}" # Getting the first value from an array - - "Completed college in: {{ inputs.payload.graduation_years | last }}" # Getting the last value from an array -``` - -This flow will log the following statements: - -``` -Student name: John Doe -Score in languages: 252 -Total subjects: 5 -Total score: 431 -Complete address: Paris, FRANCE -Total years for graduation: 4 -Started college in: 2020 -``` diff --git a/content/docs/expressions/03.filter/numeric.md b/content/docs/expressions/03.filter/numeric.md deleted file mode 100644 index 00a143c05d..0000000000 --- a/content/docs/expressions/03.filter/numeric.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -title: Numeric Filters -icon: /docs/icons/expression.svg ---- - -Numeric filters are used to format numbers or convert strings to numbers. - -Each section below represents a built-in filter. - -- [abs](#abs) -- [number](#number) -- [numberFormat](#numberformat) - -## abs - -The `abs` filter is used to obtain the absolute value. - -```twig -{{ -7 | abs }} - -{# output: 7 #} -``` - -## number - -The `number` filter return a parsed number from a string. If no type is passed, we try to infer the appropriate type. - - -```twig -{{ "12.3" | number | className }} -{# will output: java.lang.Float #} - -{{ "9223372036854775807" | number('BIGDECIMAL') | className }} -{# will output: java.math.BigDecimal #} -``` - -**Arguments** -- type: - - `INT` - - `FLOAT` - - `LONG` - - `DOUBLE` - - `BIGDECIMAL` - - `BIGINTEGER` - -## numberFormat - -The `numberFormat` filter is used to format a decimal number. Behind the scenes it uses `java.text.DecimalFormat`. -```twig -{{ 3.141592653 | numberFormat("#.##") }} -``` -The above example will output the following: -```twig -3.14 -``` - -**Arguments** -- format - -## replace - -The `replace` filter formats a given string by replacing the placeholders (placeholders are free-form) or using regular expression: -```twig -{{ "I like %this% and %that%." | replace({'%this%': foo, '%that%': "bar"}) }} -``` - -```twig -{{ 'aa1bb2cc3dd4ee5' | replace({'(\d)': '-$1-'}, regexp=true) }} -``` - -**Arguments** -- `replace_pairs`: an object with key the search string and value the replace string -- `regexp`: use regexp for search and replace pattern (default is `false`) - diff --git a/content/docs/expressions/03.filter/object.md b/content/docs/expressions/03.filter/object.md deleted file mode 100644 index 9903e7a2b1..0000000000 --- a/content/docs/expressions/03.filter/object.md +++ /dev/null @@ -1,196 +0,0 @@ ---- -title: Object Filters (Maps, Arrays and More) -icon: /docs/icons/expression.svg ---- - -Object filters help you manipulate maps and arrays. - -Each section below represents a built-in filter. - -- [chunk](#chunk) -- [className](#classname) -- [first](#first) -- [join](#join) -- [keys](#keys) -- [last](#last) -- [merge](#merge) -- [reverse](#reverse) -- [rsort](#rsort) -- [slice](#slice) -- [sort](#sort) -- [split](#split) - -## chunk - -The `chunk` filter returns partitions of `size` from a list. -```twig -{{ [[1,2,3,4,5,6,7,8,9]] | chunk(2) }} -{# results in: '[[1,2],[3,4],[5,6],[7,8],[9]]' #} -``` - -**Arguments** -- `size`: the size of the chunk - -## className - -The `className` filter return a string with the current class name. - -```twig -{{ "12.3" | number | className }} -{# will output: java.lang.Float #} -``` - -## first - -The `first` filter will return the first item of a collection, or the first letter of a string. - -```twig -{{ users | first }} -{# will output the first item in the collection named 'users' #} - -{{ 'Mitch' | first }} -{# will output 'M' #} -``` - -## join - -The `join` filter will concatenate all items of a collection into a string. An optional argument can be given -to be used as the separator between items. - -```twig -{# - List names = new ArrayList<>(); - names.add("Alex"); - names.add("Joe"); - names.add("Bob"); -#} -{{ names | join(',') }} -{# will output: Alex,Joe,Bob #} -``` - -**Arguments** -- separator - -## keys - -The `keys` filter will return the keys from a collection, or list of index of an array. - -```twig -{{ {'this': 'foo', 'that': 'bar'} | keys }} -{# will output the keys from this map '['this', 'that']' #} - - -{{ [0, 1, 3] | keys }} -{# will output the key from this array '[0, 1, 2]' #} -``` - -## values - -The `values` filter will return the values from a map: - -```twig -{{ {'this': 'foo', 'that': 'bar'} | values }} -{# will output the values from this map '['foo', 'bar']' #} -``` - -Here is how you can test that expression in a flow: - -```yaml -id: expression -namespace: company.myteam -tasks: - - id: hello - type: io.kestra.plugin.core.log.Log - message: "{{ {'this': 'foo', 'that': 'bar'} | values }}" -``` - -## last - -The `last` filter will return the last item of a collection, or the last letter of a string. -```twig -{{ users | last }} -{# will output the last item in the collection named 'users' #} - -{{ 'Mitch' | last }} -{# will output 'h' #} -``` - -## length - -The `length` filter returns the number of items of collection, map or the length of a string: - -```twig -{% if users | length > 10 %} - ... -{% endif %} -``` - -## merge - -The `merge` filter merge items of type Map, List or Array: -```twig -{{ [1, 2] | merge([3, 4]) }} -``` - -## reverse - -The `reverse` filter reverses a List: -```twig -{% for user in users | reverse %} {{ user }} {% endfor %} -``` - -## rsort - -The `rsort` filter will sort a list in reversed order. The items of the list must implement `Comparable`. -```twig -{% for user in users | rsort %} - {{ user.name }} -{% endfor %} -``` - -## slice - -The `slice` filter returns a portion of a list, array, or string. -```twig -{{ ['apple', 'peach', 'pear', 'banana'] | slice(1,3) }} -{# results in: [peach, pear] #} - - -{{ 'Mitchell' | slice(1,3) }} -{# results in: 'it' #} -``` - -**Arguments** -- `fromIndex`: 0-based and inclusive -- `toIndex`: 0-based and exclusive - -## sort - -The `sort` filter will sort a list. The items of the list must implement `Comparable`. -```twig -{% for user in users | sort %} - {{ user.name }} -{% endfor %} -``` - -## split - -The `split` filter splits a string by the given delimiter and returns a list of strings. -```twig -{% set foo = "one,two,three" | split(',') %} -{# foo contains ['one', 'two', 'three'] #} -``` - -You can also pass a limit argument: -- If `limit` is positive, then the pattern will be applied at most n - 1 times, the array's length will be no greater than n, and the array's last entry will contain all input beyond the last matched delimiter; -- If `limit` is negative, then the pattern will be applied as many times as possible and the array can have any length; -- If `limit` is zero, then the pattern will be applied as many times as possible, the array can have any length, and trailing empty strings will be discarded; - -```twig -{% set foo = "one,two,three,four,five" | split(',', 3) %} -{# foo contains ['one', 'two', 'three,four,five'] #} -``` - -**Arguments** -- delimiter: The delimiter -- limit: The limit argument diff --git a/content/docs/expressions/03.filter/string.md b/content/docs/expressions/03.filter/string.md deleted file mode 100644 index d885979f4f..0000000000 --- a/content/docs/expressions/03.filter/string.md +++ /dev/null @@ -1,332 +0,0 @@ ---- -title: String Filters -icon: /docs/icons/expression.svg ---- - -String filters are used to manipulate strings i.e. textual data. - -Each section below represents a built-in filter. - -- [abbreviate](#abbreviate) -- [base64decode](#base64decode) -- [base64encode](#base64encode) -- [capitalize](#capitalize) -- [default](#default) -- [escapeChar](#escapechar) -- [lower](#lower) -- [replace](#replace) -- [sha256](#sha256) -- [startsWith](#startswith) -- [slugify](#slugify) -- [substringAfter](#substringafter) -- [substringAfterLast](#substringafterlast) -- [substringBefore](#substringbefore) -- [substringBeforeLast](#substringbeforelast) -- [title](#title) -- [trim](#trim) -- [upper](#upper) -- [urldecode](#urldecode) -- [urlencode](#urlencode) - ---- - -## abbreviate - -The `abbreviate` filter will abbreviate a string using an ellipsis. It takes one argument which is the max -width of the desired output including the length of the ellipsis. - -```twig -{{ "this is a long sentence." | abbreviate(7) }} -``` - -The above example will output the following: - -```twig -this... -``` - -**Arguments** -- length - ---- - -## base64decode - -The `base64decode` filter takes the given input, base64-decodes it, and returns the byte array converted to UTF-8 string. - -Applying the filter on an incorrect base64-encoded string will throw an exception. - -```twig -{{ "dGVzdA==" | base64decode }} -``` - -The above example will output the following: - -``` -test -``` - ---- - -## base64encode - -The `base64encode` filter takes the given input, converts it to an UTF-8 String (`.toString()`) and Base64-encodes it. - -```twig -{{ "test" | base64encode }} -``` - -The above example will output the following: -``` -dGVzdA== -``` - ---- - -## capitalize - -The `capitalize` filter will capitalize the first letter _of the string_. -```twig -{{ "article title" | capitalize }} -``` - -The above example will output the following: - -```twig -Article title -``` - ---- - -## title - -The `title` filter will capitalize the first letter _of each word_. - -```twig -{{ "article title" | title }} -``` - -The above example will output the following: -```twig -Article Title -``` - -## default - -The `default` filter will render a default value if and only if the object being filtered is empty. -A variable is empty if it is null, an empty string, an empty collection, or an empty map. - -```twig -{{ user.phoneNumber | default("No phone number") }} -``` - -In the following example, if `foo`, `bar`, or `baz` are null the output will become an empty string which is a perfect use case for the default filter: -```twig -{{ foo.bar.baz | default("No baz") }} -``` - -Note that the default filter will suppress any `AttributeNotFoundException` exceptions that will usually be thrown. - -**Arguments** -- default - -## escapeChar - -The `escapeChar` filter sanitizes given string using a selected string escape sequence. - -Precede every `'` character with `\`: - -```twig -{{ "Can't be here" | escapeChar('single') }} -{# results in: Can\'t be here #} -``` - -Precede every `"` character with `\`: - -```twig -{{ '"Quoted"' | escapeChar('double') }} -{# results in: \"Quoted\" #} -``` - -Safely pass a rendered Pebble variable as literal value to a shell, replacing every `'` character with the `'\''` escape sequence: - -```twig -{# inputs.param value set to: Can't be here #} -echo '{{ inputs.param | escapeChar('shell') }}' -{# results in: echo 'Can'\''t be here' #} -``` - -**Arguments** - -- `type`: escape sequence type `single`, `double`, or `shell` - -## lower - -The `lower` filter makes an entire string lower case. - -```twig -{{ "THIS IS A LOUD SENTENCE" | lower }} -``` - -The above example will output the following: -```twig -this is a loud sentence -``` - -## replace - -The `replace` filter formats a given string by replacing the placeholders (placeholders are free-form) or using regular expression: -```twig -{{ "I like %this% and %that%." | replace({'%this%': foo, '%that%': "bar"}) }} -``` - -```twig -{{ 'aa1bb2cc3dd4ee5' | replace({'(\d)': '-$1-'}, regexp=true) }} -``` - -**Arguments** -- `replace_pairs`: an object with key the search string and value the replace string -- `regexp`: use regexp for search and replace pattern (default is `false`) - -## sha256 - -The `sha256` filter returns the SHA-256 hash of the given UTF-8 String. - -```twig -{{ "test" | sha256 }} -``` - -The above example will output the following: -``` -9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 -``` - -## startsWith - -The `startsWith()` filter returns `true` if the input string starts with the specified prefix. This filter is useful for string comparisons and conditional logic in your workflows. - -```yaml -id: compare_strings -namespace: company.team - -inputs: - - id: myvalue - type: STRING - defaults: "hello world!" - -tasks: - - id: log_true - type: io.kestra.plugin.core.log.Log - message: "{{ inputs.myvalue | startsWith('hello') }}" - - - id: log_false - type: io.kestra.plugin.core.log.Log - message: "{{ inputs.myvalue | startsWith('Hello') }}" -``` - -## slugify - -The `slugify` filter removes non-word characters (alphanumerics and underscores) and converts spaces to hyphen. Also strips leading and trailing whitespace. - -```twig -{{ "Joel is a slug" | slugify }} -{# will output 'joel-is-a-slug' #} -``` - -## substringAfter - -The `substringAfter` filter returns the substring before the first occurrence of a separator. The separator is not returned. - -```twig -{{ 'a.b.c' | substringAfter('.') }} -{# results in: 'b.c' #} -``` - -**Arguments** -- `separator`: the string to search for - -## substringAfterLast - -The `substringAfterLast` filter returns the substring after the last occurrence of a separator. The separator is not returned. - -```twig -{{ 'a.b.c' | substringAfterLast('.') }} -{# results in: 'c' #} -``` - -**Arguments** -- `separator`: the string to search for - -## substringBefore - -The `substringBefore` filter returns the substring before the first occurrence of a separator. The separator is not returned. -```twig -{{ 'a.b.c' | substringBefore('.') }} -{# results in: 'a' #} -``` - -**Arguments** -- `separator`: the string to search for - -## substringBeforeLast - -The `substringBeforeLast` filter returns the substring before the last occurrence of a separator. The separator is not returned. - -```twig -{{ 'a.b.c' | substringBeforeLast('.') }} -{# results in: 'a.b' #} -``` - -**Arguments** -- `separator`: the string to search for - -## trim - -The `trim` filter is used to trim whitespace off the beginning and end of a string. -```twig -{{ " This text has too much whitespace. " | trim }} -``` - -The above example will output the following: -```twig -This text has too much whitespace. -``` - -## upper - -The `upper` filter makes an entire string upper case. - -```twig -{{ "this is a quiet sentence." | upper }} -``` - -The above example will output the following: -```twig -THIS IS A QUIET SENTENCE. -``` - -## urldecode - -The `urldecode` translates a string into `application/x-www-form-urlencoded` format using the "UTF-8" encoding scheme. - -```twig -{{ "The+string+%C3%BC%40foo-bar" | urldecode }} -``` - -The above example will output the following: -```twig -The string ü@foo-bar -``` - -## urlencode - -The `urlencode` translates a string into `application/x-www-form-urlencoded` format using the "UTF-8" encoding scheme. - -```twig -{{ "The string ü@foo-bar" | urlencode }} -``` - -The above example will output the following: -```twig -The+string+%C3%BC%40foo-bar -``` \ No newline at end of file diff --git a/content/docs/expressions/03.filter/temporal.md b/content/docs/expressions/03.filter/temporal.md deleted file mode 100644 index aee39aca1d..0000000000 --- a/content/docs/expressions/03.filter/temporal.md +++ /dev/null @@ -1,180 +0,0 @@ ---- -title: Temporal Filters -icon: /docs/icons/expression.svg ---- - -Temporals filters are used to format dates and timestamps. - -- [date](#date) -- [dateAdd](#dateadd) -- [timestamp](#timestamp) -- [timestampMicro](#timestampmicro) -- [timestampNano](#timestampnano) - -## date - -The `date` filter formats a date in a variety of formats. It can handle `java.util.Date`, -Java 8 `java.time` constructs like `OffsetDateTime` and timestamps in milliseconds from the epoch. -The filter will construct a `java.text.SimpleDateFormat` or `java.time.format.DateTimeFormatter` using the provided -pattern and then use this newly created format to format the provided date object. If you don't provide a pattern, -either `DateTimeFormatter.ISO_DATE_TIME` or `yyyy-MM-dd'T'HH:mm:ssZ` will be used. - -```twig -{{ user.birthday | date("yyyy-MM-dd") }} -``` - -An alternative way to use this filter is to use it on a string but then provide two arguments: -the first is the desired pattern for the output, and the second is the existing format used to parse the -input string into a `java.util.Date` object. - -```twig -{{ "July 24, 2001" | date("yyyy-MM-dd", existingFormat="MMMM dd, yyyy") }} -``` - -The above example will output the following: -```twig -2001-07-24 -``` - -**Time zones** - -If the provided date has time zone info (e.g. `OffsetDateTime`) then it will be used. If the provided date has no -time zone info, by default the system time zone will be used. If you need to use a specific -time zone then you can pass in a `timeZone` parameter any string that's understood by `ZoneId` / `ZoneInfo`: -```twig -{# the timeZone parameter will be ignored #} -{{ someOffsetDateTime | date("yyyy-MM-dd'T'HH:mm:ssX", timeZone="UTC") }} -{# the provided time zone will override the system default #} -{{ someInstant | date("yyyy-MM-dd'T'HH:mm:ssX", timeZone="Pacific/Funafuti") }} -``` - -**format & existingFormat** - -Format can be: -- [DateTimeFormatter](https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html) format -- `iso` = `yyyy-MM-dd'T'HH:mm:ss.SSSSSSXXX` -- `iso_sec` = `yyyy-MM-dd'T'HH:mm:ssXXX` -- `sql` = `yyyy-MM-dd HH:mm:ss.SSSSSS` -- `sql_seq` = `yyyy-MM-dd HH:mm:ss` -- `iso_date_time` -- `iso_date` -- `iso_time` -- `iso_local_date` -- `iso_instant` -- `iso_local_date_time` -- `iso_local_time` -- `iso_offset_time` -- `iso_ordinal_date` -- `iso_week_date` -- `iso_zoned_date_time` -- `rfc_1123_date_time` - -**Arguments** -- `format`: the output format -- `existingFormat`: the input format if the based variable is a string -- `timeZone`: the timezone to use -- `locale`: the locale to use - -## dateAdd - -The `dateAdd` filter adds a unit and formats a date with the same behavior as [date](#date) filters. - -```twig -{{ user.birthday | dateAdd(-1, 'DAYS') }} -``` - -**Arguments** -- amount: an integer value positive or negative -- unit: - - `NANOS` - - `MICROS` - - `MILLIS` - - `SECONDS` - - `MINUTES` - - `HOURS` - - `HALF_DAYS` - - `DAYS` - - `WEEKS` - - `MONTHS` - - `YEARS` - - `DECADES` - - `CENTURIES` - - `MILLENNIA` - - `ERAS` -- format: see [date](#date) -- existingFormat [date](#date) -- timeZone [date](#date) -- locale [date](#date) - - -## timestamp - -The `timestamp` filter will convert a date to a unix timestamps in second. You can convert a string with `existingFormat` and change `timeZone` with same arguments from [date](#date) filter. - - -```twig -{{ now() | timestamp(timeZone="Europe/Paris") }} -``` - -**Arguments** -- existingFormat -- timeZone - - -## timestampMicro - -The `timestampMicro` filter will convert a date to a unix timestamps in microsecond. You can convert a string with `existingFormat` and change `timeZone` with same arguments from [date](#date) filter. - - -```twig -{{ now() | timestampMicro(timeZone="Europe/Paris") }} -``` - -**Arguments** -- existingFormat -- timeZone - -## timestampNano - -The `timestampNano` filter will convert a date to a unix timestamps in nanosecond. You can convert a string with `existingFormat` and change `timeZone` with same arguments from [date](#date) filter. - -```twig -{{ now() | timestampNano(timeZone="Europe/Paris") }} -``` - -**Arguments** -- existingFormat -- timeZone - -# Temporal Filters in action - -Let us create a sample flow to understand the temporal filters. - -```yaml -id: temporal-dates -namespace: company.myteam - -tasks: - - id: print_status - type: io.kestra.plugin.core.log.Log - message: - - "Present timestamp: {{ now() }}" - - "Formatted timestamp: {{ now() | date('yyyy-MM-dd') }}" - - "Previous day: {{ now() | dateAdd(-1, 'DAYS') }}" - - "Next day: {{ now() | dateAdd(1, 'DAYS') }}" - - "Different timezone: {{ now() | timestamp(timeZone='Asia/Kolkata') }}" - - "Different timezone (in macro): {{ now() | timestampMicro(timeZone='Asia/Kolkata') }}" - - "Different timezone (in nano): {{ now() | timestampNano(timeZone='Asia/Kolkata') }}" -``` - -The logs of this flow will be: - -``` -Present timestamp: 2024-07-09T06:17:01.171193Z -Formatted timestamp: 2024-07-09 -Previous day: 2024-07-08T06:17:01.174686Z -Next day: 2024-07-10T06:17:01.176138Z -Different timezone: 1720505821 -Different timezone (in macro): 1720505821000180275 -Different timezone (in nano): 1720505821182413000 -``` diff --git a/content/docs/expressions/03.filter/yaml.md b/content/docs/expressions/03.filter/yaml.md deleted file mode 100644 index 6c8b50f3eb..0000000000 --- a/content/docs/expressions/03.filter/yaml.md +++ /dev/null @@ -1,678 +0,0 @@ ---- -title: YAML Filters -icon: /docs/icons/expression.svg ---- - -YAML filters are used to turn YAML strings into objects. - - -## yaml - -This filter, added in [kestra 0.16.0](https://github.com/kestra-io/kestra/pull/3283), is used to parse a YAML string into an object. That object can then be transformed using the Pebble templating engine. - -The filter is useful when working with the [TemplatedTask](/plugins/tasks/templating/io.kestra.plugin.core.templating.TemplatedTask) added in [#3191](https://github.com/kestra-io/kestra/pull/3191). - - -```twig -{{ "foo: bar" | yaml }} -``` - -::collapse{title="Full workflow example using the filter in a templated task"} - -```yaml -id: yaml_filter -namespace: company.team - -labels: - label: value - -inputs: - - name: string - type: STRING - required: false - defaults: "string" - - - name: int - type: INT - required: false - defaults: 123 - - - name: bool - type: BOOLEAN - required: false - defaults: true - - - name: float - type: FLOAT - required: false - defaults: 1.23 - - - name: instant - type: DATETIME - required: false - defaults: "1918-02-24T01:02:03.04Z" - - - name: date - type: DATE - required: false - defaults: "1991-08-20" - - - name: json - type: JSON - required: false - defaults: | - { - "string": "string", - "integer": 123, - "float": 1.23, - "boolean": false, - "null": null, - "object": {}, - "array": [ - "string", - 123, - 1.23, - false, - null, - {}, - [] - ] - } - - - name: uri - type: URI - required: false - defaults: "https://kestra.io" - - - name: nested.object - type: STRING - required: false - defaults: "value" - - - name: nested.object.child - type: STRING - required: false - defaults: "value" - -variables: - string: "string" - int: 123 - bool: true - float: 1.23 - instant: "1918-02-24T00:00:00Z" - date: "1991-08-20" - time: "23:59:59" - duration: "PT5M6S" - json: - string: string - integer: 123 - float: 1.23 - boolean: false - 'null': - object: {} - array: - - string - - 123 - - 1.23 - - false - - - - {} - - [] - uri: "https://kestra.io" - object: - key: "value" - child: - key: "value" - array: - - Value 1 - - Value 2 - - Value 3 - yaml: | - string: string - integer: 123 - float: 1.23 - boolean: false - 'null': - object: {} - array: - - string - - 123 - - 1.23 - - false - - - - {} - - [] - -tasks: - - id: yaml_filter - type: "io.kestra.plugin.core.log.Log" - message: | - {{ "string" | yaml }} - {{ 1 | yaml }} - {{ 1.123 | yaml }} - {{ true | yaml }} - {{ 1 | yaml }} - {{ [1, true, "string", [0, false, "string"]] | yaml }} - {{ {"key": "value", "object": {"a":"b"}} | yaml }} - - - id: yaml_kestra - type: "io.kestra.plugin.core.log.Log" - disabled: false - message: | - --- - # flow - {{ flow ?? null | yaml }} - --- - # execution - {{ execution ?? null | yaml }} - --- - # task - {{ task ?? null | yaml }} - --- - # taskrun - {{ taskrun ?? null | yaml }} - --- - # parent - {{ parent ?? null | yaml }} - --- - # parents - {{ parents ?? null | yaml }} - --- - # trigger - {{ trigger ?? null | yaml }} - --- - # vars - {{ vars ?? null | yaml }} - --- - # inputs - {{ inputs ?? null | yaml }} - --- - # outputs - {{ outputs ?? null | yaml }} - --- - # labels - {{ labels ?? null | yaml }} - - - - id: yaml_function - type: "io.kestra.plugin.core.log.Log" - message: | - {{ yaml(vars.yaml) }} - - - id: yaml2yaml - type: "io.kestra.plugin.core.log.Log" - message: | - {{ yaml(vars.yaml) | yaml }} -``` -:: - -## indent - -When constructing YAML from multiple objects, this filter can apply indentation to strings, adding `amount` number of spaces before each line except for the first line. - -The `prefix` property defines what is used to indent the lines. By default, prefix is `" "` (a space). - -The syntax: - -```twig -indent(amount, prefix=" ") -``` - -## nindent - -Use `nindent` to add a new line (hence the `n` in `nindent`) before the code and then indent all lines by adding `amount` number of spaces before each line. The `amount` is a required property defining how many times prefix is repeated before each line. - -The `prefix` property defines what is used to indent the lines. By default, prefix is `" "` (a space). - -The syntax: - -```twig -nindent(amount, prefix=" ") -``` - -::collapse{title="Full workflow example using the indent filter"} - -```yaml -id: templated_task -namespace: company.team - -labels: - my-label: "will-be-sent-to-k8s" - other-label: "value" - -inputs: - # using nested inputs to define types and default values for `resources` object - - name: resources.limits.cpu - type: STRING - defaults: "1" - required: true - - - name: resources.limits.memory - type: STRING - defaults: "1000Mi" - required: true - - - name: resources.requests.cpu - type: STRING - required: false # optional; it can be skipped when running the flow - - - name: resources.requests.memory - type: STRING - required: false # optional; it can be skipped when running the flow - -# using variables to explicitly define lists -variables: - env: # is presented as a list of objects - - name: "var_string" - value: "VALUE" - - name: "var_int" - value: 123 - -tasks: - - id: dynamic - type: io.kestra.plugin.core.templating.TemplatedTask - spec: | - id: "{{ flow.namespace | slugify }}-{{ flow.id | slugify }}" - type: io.kestra.plugin.kubernetes.PodCreate - namespace: kestra - metadata: - labels: - {{ labels | yaml | indent(4) }} # indenting from the next line, 4 spaces here - spec: - containers: - - name: debug - image: alpine:latest - env: {{ variables.env | yaml | indent(6) }} # adding a newline and indenting at 6 spaces - command: - - 'bash' - - '-c' - - 'printenv' - resources: - {{ inputs.resources | yaml | indent(2, " ") }} # indenting from the next line by 2 times " " (two spaces) -``` -:: - - -::collapse{title="Sample code for testing the indent filter"} - -```yaml -id: indent_filter -namespace: company.team - -labels: - label: value - -inputs: - - id: string - type: STRING - required: false - defaults: "string" - - - id: int - type: INT - required: false - defaults: 123 - - - id: bool - type: BOOLEAN - required: false - defaults: true - - - id: float - type: FLOAT - required: false - defaults: 1.23 - - - id: instant - type: DATETIME - required: false - defaults: "1918-02-24T01:02:03.04Z" - - - id: date - type: DATE - required: false - defaults: "1991-08-20" - - - id: json - type: JSON - required: false - defaults: | - { - "string": "string", - "integer": 123, - "float": 1.23, - "boolean": false, - "null": null, - "object": {}, - "array": [ - "string", - 123, - 1.23, - false, - null, - {}, - [] - ] - } - - - id: uri - type: URI - required: false - defaults: "https://kestra.io" - -variables: - inputEmptyLines: "\n\n" - inputString: 'string' - inputInteger: 1 - inputStringWithCRLF: "first line\r\nsecond line" - inputStringWithLF: "first line\nsecond line" - inputStringWithCR: "first line\rsecond line" - inputWithTab: "first line\nsecond line" - - indentEmptyLines: "\n \n " - indentString: "string" - indentInteger: "1" - indentStringWithCRLF: "first line\r\n second line" - indentStringWithLF: "first line\n second line" - indentStringWithCR: "first line\r second line" - indentWithTab: "first line\n\t\tsecond line" - -tasks: - - id: debug - description: "Debug task run" - type: "io.kestra.plugin.core.log.Log" - level: INFO - message: | - { - "flow" : {{ flow ?? 'null' }}, - "execution" : {{ execution ?? 'null' }}, - "task" : {{ task ?? 'null' }}, - "taskrun" : {{ taskrun ?? 'null' }}, - "parent" : {{ parent ?? 'null' }}, - "parents" : {{ parents ?? 'null' }}, - "trigger" : {{ trigger ?? 'null' }}, - "vars" : {{ vars ?? 'null' }}, - "inputs" : {{ inputs ?? 'null' }}, - "outputs" : {{ outputs ?? 'null' }}, - "labels" : {{ labels ?? 'null' }} - } - - - id: inputs-string - type: "io.kestra.plugin.core.log.Log" - message: | - {{ (inputs.string | indent(2)) == "" + inputs.string }} - {{ inputs.string | indent(2) }} - - - id: inputs-int - type: "io.kestra.plugin.core.log.Log" - message: | - {{ (inputs.int | indent(2)) == "" + inputs.int }} - {{ inputs.int | indent(2) }} - - - id: inputs-bool - type: "io.kestra.plugin.core.log.Log" - message: | - {{ (inputs.bool | indent(2)) == "" + inputs.bool }} - {{ inputs.bool | indent(2) }} - - - id: inputs-float - type: "io.kestra.plugin.core.log.Log" - message: | - {{ (inputs.float | indent(2)) == "" + inputs.float }} - {{ inputs.float | indent(2) }} - - - id: inputs-instant - type: "io.kestra.plugin.core.log.Log" - message: | - {{ (inputs.instant | indent(2)) == "" + inputs.instant }} - {{ inputs.instant | indent(2) }} - - - id: inputs-date - type: "io.kestra.plugin.core.log.Log" - message: | - {{ (inputs.date | indent(2)) == "" + inputs.date }} - {{ inputs.date | indent(2) }} - - - id: inputs-json - type: "io.kestra.plugin.core.log.Log" - message: | - {{ (inputs.json | indent(2)) == "" + inputs.json }} - {{ inputs.json | indent(2) }} - - - id: inputs-uri - type: "io.kestra.plugin.core.log.Log" - message: | - {{ (inputs.uri | indent(2)) == "" + inputs.uri }} - {{ inputs.uri | indent(2) }} - - - id: variables-inputNull - type: "io.kestra.plugin.core.log.Log" - message: | - {{ (null | indent(2)) == "" }} - {{ (null | indent(2)) }} - - - id: variables-inputEmpty - type: "io.kestra.plugin.core.log.Log" - message: | - {{ ("" | indent(2)) == "" }} - {{ ("" | indent(2)) }} - - - id: variables-inputEmptyLines - type: "io.kestra.plugin.core.log.Log" - message: | - {{ (vars.inputEmptyLines | indent(2)) == vars.indentEmptyLines }} - {{ (vars.inputEmptyLines | indent(2)) }} - - - id: variables-inputString - type: "io.kestra.plugin.core.log.Log" - message: | - {{ (vars.inputString | indent(2)) == vars.indentString }} - {{ (vars.inputString | indent(2)) }} - - - id: variables-inputInteger - type: "io.kestra.plugin.core.log.Log" - message: | - {{ (vars.inputInteger | indent(2)) == vars.indentInteger }} - {{ (vars.inputInteger | indent(2)) }} - - - id: variables-inputStringWithCRLF - type: "io.kestra.plugin.core.log.Log" - message: | - {{ (vars.inputStringWithCRLF | indent(2)) == vars.indentStringWithCRLF }} - {{ (vars.inputStringWithCRLF | indent(2)) }} - - - id: variables-inputStringWithLF - type: "io.kestra.plugin.core.log.Log" - message: | - {{ (vars.inputStringWithLF | indent(2)) == vars.indentStringWithLF }} - {{ (vars.inputStringWithLF | indent(2)) }} - - - id: variables-inputStringWithCR - type: "io.kestra.plugin.core.log.Log" - message: | - {{ (vars.inputStringWithCR | indent(2)) == vars.indentStringWithCR }} - {{ (vars.inputStringWithCR | indent(2)) }} - - - id: variables-inputWithTab - type: "io.kestra.plugin.core.log.Log" - message: | - {{ (vars.inputWithTab | indent(2)) == vars.indentWithTab }} - {{ (vars.inputWithTab | indent(2, "\t")) }} -``` -:: - -::collapse{title="Sample code for testing the nindent filter"} - -```yaml -id: nindent_filter -namespace: company.team - -labels: - label: value - -inputs: - - id: string - type: STRING - required: false - defaults: "string" - - - id: int - type: INT - required: false - defaults: 123 - - - id: bool - type: BOOLEAN - required: false - defaults: true - - - id: float - type: FLOAT - required: false - defaults: 1.23 - - - id: instant - type: DATETIME - required: false - defaults: "1918-02-24T01:02:03.04Z" - - - id: date - type: DATE - required: false - defaults: "1991-08-20" - - - id: json - type: JSON - required: false - defaults: | - { - "string": "string", - "integer": 123, - "float": 1.23, - "boolean": false, - "null": null, - "object": {}, - "array": [ - "string", - 123, - 1.23, - false, - null, - {}, - [] - ] - } - - - id: uri - type: URI - required: false - defaults: "https://kestra.io" - -variables: - inputEmptyLines: "\n\n" - inputString: 'string' - inputInteger: 1 - inputStringWithCRLF: "first line\r\nsecond line" - inputStringWithLF: "first line\nsecond line" - inputStringWithCR: "first line\rsecond line" - inputWithTab: "first line\nsecond line" - - nindentEmptyLines: "\n \n \n " - nindentString: "\n string" - nindentInteger: "\n 1" - nindentStringWithCRLF: "\r\n first line\r\n second line" - nindentStringWithLF: "\n first line\n second line" - nindentStringWithCR: "\r first line\r second line" - nindentWithTab: "\n\t\tfirst line\n\t\tsecond line" - - -tasks: - - id: debug - description: "Debug task run" - type: "io.kestra.plugin.core.log.Log" - level: INFO - # disabled: true - message: | - { - "flow" : {{ flow ?? 'null' }}, - "execution" : {{ execution ?? 'null' }}, - "task" : {{ task ?? 'null' }}, - "taskrun" : {{ taskrun ?? 'null' }}, - "parent" : {{ parent ?? 'null' }}, - "parents" : {{ parents ?? 'null' }}, - "trigger" : {{ trigger ?? 'null' }}, - "vars" : {{ vars ?? 'null' }}, - "inputs" : {{ inputs ?? 'null' }}, - "outputs" : {{ outputs ?? 'null' }}, - "labels" : {{ labels ?? 'null' }} - } - - - id: inputs-string - type: "io.kestra.plugin.core.log.Log" - message: | - {{ (inputs.string | nindent(2)) == "" + inputs.string }} - {{ inputs.string | nindent(2) }} - - - id: inputs-int - type: "io.kestra.plugin.core.log.Log" - message: | - {{ (inputs.int | nindent(2)) == "" + inputs.int }} - {{ inputs.int | nindent(2) }} - - - id: inputs-bool - type: "io.kestra.plugin.core.log.Log" - message: | - {{ (inputs.bool | nindent(2)) == "" + inputs.bool }} - {{ inputs.bool | nindent(2) }} - - - id: inputs-float - type: "io.kestra.plugin.core.log.Log" - message: | - {{ (inputs.float | nindent(2)) == "" + inputs.float }} - {{ inputs.float | nindent(2) }} - - - id: inputs-instant - type: "io.kestra.plugin.core.log.Log" - message: | - {{ (inputs.instant | nindent(2)) == "" + inputs.instant }} - {{ inputs.instant | nindent(2) }} - - - id: inputs-date - type: "io.kestra.plugin.core.log.Log" - message: | - {{ (inputs.date | nindent(2)) == "" + inputs.date }} - {{ inputs.date | nindent(2) }} - - - id: inputs-json - type: "io.kestra.plugin.core.log.Log" - message: | - {{ (inputs.json | nindent(2)) == "" + inputs.json }} - {{ inputs.json | nindent(2) }} - - - id: inputs-uri - type: "io.kestra.plugin.core.log.Log" - message: | - {{ (inputs.uri | nindent(2)) == "" + inputs.uri }} - {{ inputs.uri | nindent(2) }} - - - id: variables-inputNull - type: "io.kestra.plugin.core.log.Log" - message: | - {{ (null | nindent(2)) == "" }} - {{ (null | nindent(2)) }} - - - id: variables-inputEmpty - type: "io.kestra.plugin.core.log.Log" - message: | - {{ ("" | nindent(2)) == "" }} - {{ ("" | nindent(2)) }} - - - id: variables-inputEmptyLines - type: "io.kestra.plugin.core.log.Log" - message: | - {{ (vars.inputEmptyLines | nindent(2)) == vars.nindentEmptyLines }} - {{ (vars.inputEmptyLines | nindent(2)) }} - - - id: variables-inputString - type: "io.kestra.plugin.core.log.Log" - message: | - {{ (vars.inputString | nindent(2)) == vars.nindentString }} - {{ (vars.inputString | nindent(2)) }} - - - id: variables-inputInteger - type: "io.kestra.plugin.core.log.Log" - message: | - {{ (vars.inputInteger | nindent(2)) == vars.nindentInteger }} - {{ (vars.inputInteger | nindent(2)) }} - - - id: variables-inputStringWithCRLF - type: "io.kestra.plugin.core.log.Log" - message: | - {{ (vars.inputStringWithCRLF | nindent(2)) == vars.nindentStringWithCRLF }} - {{ (vars.inputStringWithCRLF | nindent(2)) }} - - - id: variables-inputStringWithLF - type: "io.kestra.plugin.core.log.Log" - message: | - {{ (vars.inputStringWithLF | nindent(2)) == vars.nindentStringWithLF }} - {{ (vars.inputStringWithLF | nindent(2)) }} - - - id: variables-inputStringWithCR - type: "io.kestra.plugin.core.log.Log" - message: | - {{ (vars.inputStringWithCR | nindent(2)) == vars.nindentStringWithCR }} - {{ (vars.inputStringWithCR | nindent(2)) }} - - - id: variables-inputWithTab - type: "io.kestra.plugin.core.log.Log" - message: | - {{ (vars.inputWithTab | nindent(2)) == vars.nindentWithTab }} - {{ (vars.inputWithTab | nindent(2, "\t")) }} -``` -:: \ No newline at end of file diff --git a/content/docs/expressions/04.function.md b/content/docs/expressions/04.function.md deleted file mode 100644 index df9e66c1a4..0000000000 --- a/content/docs/expressions/04.function.md +++ /dev/null @@ -1,571 +0,0 @@ ---- -title: Function -icon: /docs/icons/expression.svg ---- - -Functions can be called to generate content. Functions are called by their name followed by parentheses `()` and may have arguments. - -For instance, the range function returns a list containing an arithmetic progression of integers: - -```twig -{% for i in range(0, 3) %} -{{ i }}, -{% endfor %} -``` - -Each header below represents a built-in function. - -## block - -The `block` function is used to render the contents of a block more than once. It is not to be confused -with the block *tag* which is used to declare blocks. - -The following example will render the contents of the "post" block twice; once where it was declared -and again using the `block` function: -```twig -{% block "post" %} content {% endblock %} - -{{ block("post") }} -``` -The above example will output the following: -``` -content - -content -``` - -## currentEachOutput - -The `currentEachOutput` function retrieves the current output of a sibling task when using an [EachSequential](/plugins/core/tasks/flows/io.kestra.plugin.core.flow.EachSequential) task. - -Look at the following flow: - -```yaml -tasks: - - id: each - type: io.kestra.plugin.core.flow.EachSequential - tasks: - - id: first - type: io.kestra.plugin.core.debug.Return - format: "{{task.id}}" - - id: second - type: io.kestra.plugin.core.debug.Return - format: "{{ outputs.first[taskrun.value].value }}" - value: ["value 1", "value 2", "value 3"] -``` - -To retrieve the output of the `first` task from the `second` task, you need to use the special `taskrun.value` variable to lookup for the execution of the `first` task that is on the same sequential execution as the `second` task. -And when there are multiple levels of EachSequential, you must use the special `parents` variable to lookup the correct execution. For example, `outputs.first[parents[1].taskrun.value][parents[0].taskrun.value]` for a 3-level EachSequential. - -The `currentEachOutput` function will facilitate this by looking up the current output of the sibling task, so you don't need to use the special variables `taskrun.value` and `parents`. - -The previous example can be rewritten as follow: - -```yaml -tasks: - - id: each - type: io.kestra.plugin.core.flow.EachSequential - tasks: - - id: first - type: io.kestra.plugin.core.debug.Return - format: "{{task.id}}" - - id: second - type: io.kestra.plugin.core.debug.Return - format: "{{ currentEachOutput(outputs.first).value }}" - value: ["value 1", "value 2", "value 3"] -``` - -And this works no matter the number of levels of EachSequential tasks used. - -## fromJson - -The `fromJson` function will convert any JSON string to an object allowing to access its properties using the dot notation: - -```twig -{{ fromJson('[1, 2, 3]')[0] }} -{# results in: '1' #} - -{{ fromJson('{"foo": [666, 1, 2]}').foo[0] }} -{# results in: '666' #} - -{{ fromJson('{"bar": "\u0063\u0327"}').bar }} -{# results in: 'ç' #} -``` - -::alert{type="info"} -If you were using Kestra in a version prior to [v0.18.0](/blogs/2024-08-06-release-0-18.md), this function used to be named `json()`. We've renamed it to `fromJson` for more clarity. The renaming has been implemented in a non-breaking way — using `json()` will raise a warning in the UI but it will still work. -:: - - -## yaml - -The `yaml` function will convert any string to an object allowing to access its properties. - -```twig -"{{ yaml('foo: [666, 1, 2]').foo[0] }}" -{# results in: '666' #} - -{{ yaml(yaml_object) | jq(...) | yaml }} -{# prints the yaml_object as a yaml string #} -``` - -## max - -The `max` function will return the largest of it's numerical arguments. -```twig -{{ max(user.age, 80) }} -``` - -## min - -The `min` function will return the smallest of it's numerical arguments. -```twig -{{ min(user.age, 80) }} -``` - -## now - -The `now` function will return the actual datetime. The arguments are the same as the [date](./03.filter/temporal.md#date) filter except the format is different. - -```twig -{{ now() }} -{{ now(timeZone="Europe/Paris") }} -``` - -**Arguments** -- existingFormat -- timeZone -- locale - -## parent - -The `parent` function is used inside of a block to render the content that the parent template would -have rendered inside of the block had the current template not overridden it. It is similar to Java's `super` keyword. - -Let's assume you have a template, "parent.peb" that looks something like this: -```twig -{% block "content" %} - parent contents -{% endblock %} -``` -And then you have another template, "child.peb" that extends "parent.peb": -```twig -{% extends "parent.peb" %} - -{% block "content" %} - child contents - {{ parent() }} -{% endblock %} -``` -The output will look something like the following: -```twig -parent contents -child contents -``` - -## range - -The `range` function will return a list containing an arithmetic progression of numbers: -```twig -{% for i in range(0, 3) %} - {{ i }}, -{% endfor %} - -{# outputs 0, 1, 2, 3, #} -``` - -When step is given (as the third parameter), it specifies the increment (or decrement): -```twig -{% for i in range(0, 6, 2) %} - {{ i }}, -{% endfor %} - -{# outputs 0, 2, 4, 6, #} -``` - -Pebble built-in .. operator is just a shortcut for the range function with a step of 1+ -```twig -{% for i in 0..3 %} - {{ i }}, -{% endfor %} - -{# outputs 0, 1, 2, 3, #} -``` - -## printContext - -The `printContext` function is used to debug and print all variables defined. - -```twig - -{{ printContext() }} -``` -The above example will output the following: -```json -{"outputs": {...}, "execution": {...}, ...} -``` - -## read - -Read an internal storage file and return its content as a string. This function accepts one of the following: -1. A path to a Namespace File e.g. `{{ read('myscript.py') }}` -2. An internal storage URI e.g. `{{ read(inputs.myfile) }}` or `{{ read(outputs.extract.uri) }}`. - -Reading namespace files is restricted to **files in the same namespace** as the flow using this function. - -Reading internal storage files is restricted to the **current execution**. Specifically, those are files created by the current flow's execution or the parent flow execution (for flows triggered by a [Subflow](/plugins/core/tasks/flows/io.kestra.plugin.core.flow.Subflow) task or a [ForEachItem](/plugins/core/tasks/flows/io.kestra.plugin.core.flow.ForEachItem) task). - -```twig -# Read a namespace file from the path `subdir/file.txt` -{{ read('subdir/file.txt') }} - -# Read an internal storage file from the `uri` output of the `readFile` task -{{ read(outputs.readFile.uri) }} - -# Read an internal storage file from an input named `file` -{{ read(inputs.file) }} -``` - -## render - -By default, kestra renders all expressions [only once](../11.migration-guide/0.14.0/recursive-rendering.md). This is safer from a security perspective, and it prevents unintended behavior when parsing JSON elements of a webhook payload that contained a templated string from other applications (such as GitHub Actions or dbt core). However, sometimes recursive rendering is desirable. For example, if you want to parse flow variables that contain Pebble expressions. This is where the `render()` function comes in handy. - -The `render()` function is used to enable recursive rendering of Pebble expressions. It is available since the release 0.14.0. - -The syntax for the `render()` function is as follows: - -```twig -{{ render(expression_string, recursive=true) }} # if false, render only once -``` - -The function takes two arguments: -1. The string of an expression to be rendered e.g. `"{{ trigger.date ?? execution.startDate | date('yyyy-MM-dd') }}"` -2. A boolean flag that controls whether the rendering should be recursive or not: - - if `true` (default), the expression will be **rendered recursively** - - if `false`, the expression will be **rendered only once**. - -Let's see some examples of how the `render()` function works and where you need to use it. - -### Where the `render()` function is not needed - -Let's take the following flow as an example: - -```yaml -id: parse_input_and_trigger_expressions -namespace: company.team - -inputs: - - id: myinput - type: STRING - defaults: hello - -tasks: - - id: parse_date - type: io.kestra.plugin.core.debug.Return - format: "{{ trigger.date ?? execution.startDate | date('yyyy-MM-dd') }}" - - - id: parse_input - type: io.kestra.plugin.core.debug.Return - format: "{{ inputs.myinput }}" - -triggers: - - id: schedule - type: io.kestra.plugin.core.trigger.Schedule - cron: "* * * * *" -``` - -Since we don't use any nested expressions (like using trigger or input expressions in variables, or using variables in other variables), this flow will work just fine without having to use the `render()` function. - -### Where the `render()` function is needed - -Let's modify our example so that now we store that long expression ``"{{ trigger.date ?? execution.startDate | date('yyyy-MM-dd') }}"`` in a variable: - -```yaml -id: hello -namespace: company.team - -variables: - trigger_var: "{{ trigger.date ?? execution.startDate | date('yyyy-MM-dd') }}" - -tasks: - - id: parse_date - type: io.kestra.plugin.core.debug.Return - format: "{{ vars.trigger_var }}" # no render function means no recursive rendering for that trigger_var variable - -triggers: - - id: schedule - type: io.kestra.plugin.core.trigger.Schedule - disabled: true - cron: "* * * * *" -``` - -Here, the task `parse_date` will print the expression without recursively rendering it, so the printed output will be a string of that variable rather than a parsed date: - -```twig -{{ trigger.date ?? execution.startDate | date('yyyy-MM-dd') }} -``` - -To fix that, simply wrap the `vars.trigger_var` variable in the `render()` function: - -```yaml -id: hello -namespace: company.team - -variables: - trigger_var: "{{ trigger.date ?? execution.startDate | date('yyyy-MM-dd') }}" - -tasks: - - id: parse_date - type: io.kestra.plugin.core.debug.Return - format: "{{ render(vars.trigger_var) }}" # this will print the recursively-rendered expression - -triggers: - - id: schedule - type: io.kestra.plugin.core.trigger.Schedule - cron: "* * * * *" -``` - -Now you should see the date printed in the logs, e.g. `2024-01-16`. - -### Using expressions in namespace variables - -Let's assume that you have configured a [namespace variable](./02.expression-types.md#namespace-variables-ee) `myvar`. That variable uses a Pebble function to retrieve a secret e.g. `{{ secret('MY_SECRET') }}`. By default, kestra will only render the expression without recursively rendering what's inside of the namespace variable: - -```yaml -id: namespace_variable -namespace: company.team - -tasks: - - id: print_variable - type: io.kestra.plugin.core.debug.Return - format: "{{ namespace.myvar }}" -``` - -If you want to render the secret contained in that variable, you will need to wrap it in the `render()` function: - -```yaml -id: namespaces_variable -namespace: company.team - -tasks: - - id: print_variable - type: io.kestra.plugin.core.debug.Return - format: "{{ render(namespace.myvar) }}" -``` - -### String concatenation - -Let's look at another example that will demonstrate how the `render()` function works with string concatenation. - -```yaml -id: pebble_string_concatenation -namespace: company.team - -inputs: - - id: date - type: DATETIME - defaults: 2024-02-24T22:00:00.000Z - - - id: user - type: STRING - defaults: Rick - -variables: - day_of_week: "{{ trigger.date ?? inputs.date | date('EEEE') }}" - full_date: "{{ vars.day_of_week }}, the {{ trigger.date ?? inputs.date | date('yyyy-MM-dd') }}" - full_date_concat: "{{ vars.day_of_week ~ ', the ' ~ (trigger.date ?? inputs.date | date('yyyy-MM-dd')) }}" - greeting_concat: "{{'Hello, ' ~ inputs.user ~ ' on ' ~ vars.full_date }}" - greeting_brackets: "Hello, {{ inputs.user }} on {{ vars.full_date }}" - -tasks: - - id: not-rendered - type: io.kestra.plugin.core.log.Log - message: | - Concat: {{ vars.greeting_concat }} - Brackets: {{ vars.greeting_brackets }} - Full date: {{ vars.full_date }} - Full date concat: {{ vars.full_date_concat }} - - - id: rendered-recursively - type: io.kestra.plugin.core.log.Log - message: | - Concat: {{ render(vars.greeting_concat) }} - Brackets: {{ render(vars.greeting_brackets) }} - Full date: {{ render(vars.full_date) }} - Full date concat: {{ render(vars.full_date_concat) }} - - - id: rendered-once - type: io.kestra.plugin.core.log.Log - message: | - Concat: {{ render(vars.greeting_concat, recursive=false) }} - Brackets: {{ render(vars.greeting_brackets, recursive=false) }} - Full date: {{ render(vars.full_date, recursive=false) }} - Full date concat: {{ render(vars.full_date_concat, recursive=false) }} - -triggers: - - id: schedule - type: io.kestra.plugin.core.trigger.Schedule - cron: "* * * * *" -``` - -Note that: -- the ``??`` syntax within `"{{ trigger.date ?? inputs.date | date('EEEE') }}"` is a [null-coalescing operator](./05.operator.md#null-coalescing) that returns the first non-null value in the expression. For example, if `trigger.date` is null, the expression will return `inputs.date`. -- the `~` sign is a [string concatenation operator](./05.operator.md#concat) that combines two strings into one. - -When you run this flow, you should see the following output in the logs: - -``` -INFO Concat: {{'Hello, ' ~ inputs.user ~ ' on ' ~ vars.full_date }} -Brackets: Hello, {{ inputs.user }} on {{ vars.full_date }} -Full date: {{ vars.day_of_week }}, the {{ trigger.date ?? inputs.date | date('yyyy-MM-dd') }} -Full date concat: {{ vars.day_of_week ~ ', the ' ~ (trigger.date ?? inputs.date | date('yyyy-MM-dd')) }} - -INFO Concat: Hello, Rick on Saturday, the 2024-02-24 -Brackets: Hello, Rick on Saturday, the 2024-02-24 -Full date: Saturday, the 2024-02-24 -Full date concat: Saturday, the 2024-02-24 - -INFO Concat: Hello, Rick on {{ vars.day_of_week }}, the {{ trigger.date ?? inputs.date | date('yyyy-MM-dd') }} -Brackets: Hello, Rick on {{ vars.day_of_week }}, the {{ trigger.date ?? inputs.date | date('yyyy-MM-dd') }} -Full date: {{ trigger.date ?? inputs.date | date('EEEE') }}, the 2024-02-24 -Full date concat: {{ trigger.date ?? inputs.date | date('EEEE') }}, the 2024-02-24 -``` - -You may notice that both the ``vars.greeting_concat`` and ``vars.greeting_brackets`` lead to **the same output**, even though the first one uses the `~` sign for string concatenation within a single Pebble expression `{{ }}`, and the second one uses one string with multiple `{{ }}` expressions to concatenate the strings. Both are fully supported and you can decide which one to use based on your preference. - - -### Webhook trigger: using `render()` with the `recursive=false` flag - -Let's look at how the `render()` function works with the webhook trigger. - -Imagine you use a GitHub webhook as a trigger in order to automate deployments after a pull request is merged. Your GitHub webhook payload looks as follows: - -```json -{ - "pull_request": { - "html_url": "https://github.com/kestra-io/kestra/pull/2834", - "body": "This PR replaces the ${{ env.GITHUB_TOKEN }} with a more secure ${{ secrets.GITHUB_TOKEN }}." - } -} -``` - -The pull request body contains templated variables `${{ env.MYENV }}` and `${{ secrets.GITHUB_TOKEN }}`, which are not meant to be rendered by Kestra, but by GitHub Actions. Processing this webhook payload with recursive rendering would result in an error, as those variables are not defined in the flow execution context. - -In order to retrieve elements such as the `pull_request.body` from that webhook's payload without recursively rendering its content, you can leverage the `render()` function with the `recursive=false` flag: - -```yaml -id: pebble_in_webhook -namespace: company.team - -inputs: - - id: github_url - type: STRING - defaults: https://github.com/kestra-io/kestra/pull/2834 - - - id: body - type: JSON - defaults: | - { - "pull_request": { - "html_url": "https://github.com/kestra-io/kestra/pull/2834", - "body": "This PR replaces the ${{ env.GITHUB_TOKEN }} with a more secure ${{ secrets.GITHUB_TOKEN }}" - } - } - -variables: - body: "{{ trigger.body.pull_request.body ?? trigger.body.issue.body ?? inputs.body }}" - github_url: "{{ trigger.body.pull_request.html_url ?? trigger.body.issue.html_url ?? inputs.github_url }}" - -tasks: - - id: render_once - type: io.kestra.plugin.core.log.Log - message: "{{ render(vars.body, recursive=false) }}" - - - id: not_recursive - type: io.kestra.plugin.core.log.Log - message: "{{ vars.body }}" - - - id: recursive - type: io.kestra.plugin.core.log.Log - message: "{{ render(vars.body) }}" - allowFailure: true # this task will fail because it will try to render the webhook's payload - -triggers: - - id: webhook - type: io.kestra.plugin.core.trigger.Webhook - key: test1234 -``` - -Only the `render_once` task is relevant for this use case, as it will render the pull request's `body` without recursively rendering its content. The flow includes a recursive and non-recursive configuration for easy comparison. -- The `not_recursive` task will print the `{{ trigger.body.pull_request.body ?? trigger.body.issue.body ?? inputs.body }}` expression as a string without rendering it. -- The `recursive` task will fail, as it will try to render the webhook's payload containing templating that cannot be parsed by kestra. - -Here is how you can call that flow via curl: - -```shell -curl -i -X POST -H "Content-Type: application/json" \ - -d '{ "pull_request": {"html_url": "https://github.com/kestra-io/kestra/pull/2834", "body": "This PR replaces the ${{ env.GITHUB_TOKEN }} with a more secure ${{ secrets.GITHUB_TOKEN }}"} }' \ - http://localhost:8080/api/v1/executions/webhook/qa/pebble_in_webhook/test1234 -``` - -On an instance with multi-tenancy enabled, make sure to also pass the tenant ID in the URL: - -```shell -curl -i -X POST -H "Content-Type: application/json" \ - -d '{ "pull_request": {"html_url": "https://github.com/kestra-io/kestra/pull/2834"}, "body": "This PR replaces the ${{ env.GITHUB_TOKEN }} with a more secure ${{ secrets.GITHUB_TOKEN }}"} }' \ - http://localhost:8080/api/v1/your_tenant/executions/webhook/qa/pebble_in_webhook/test1234 -``` - -You should see similar output in the logs: - -``` -INFO This PR replaces the ${{ env.GITHUB_TOKEN }} with a more secure ${{ secrets.GITHUB_TOKEN }} -INFO {{ trigger.body.pull_request.body ?? trigger.body.issue.body ?? inputs.body }} -ERROR io.pebbletemplates.pebble.error.PebbleException: Missing variable: 'env' on 'This PR replaces the ${{ env.GITHUB_TOKEN }} with a more secure ${{ secrets.GITHUB_TOKEN }}' at line 1 (?:?) -ERROR Missing variable: 'env' on 'This PR replaces the ${{ env.GITHUB_TOKEN }} with a more secure ${{ secrets.GITHUB_TOKEN }}' at line 1 (?:?) -``` - -## renderOnce - -The `renderOnce()` function is used to enable one-time rendering of nested Pebble expressions. It is available since the release 0.16.0 and is equivalent to [render(expression_string, recursive=false)](#render). - -This function is syntactic sugar to reduce overhead brought by the recursive rendering default behaviour. It can be used to use `vars` easily as they may contain themselves pebble expressions. - -Basically, if `vars.a={{ vars.b }}`, `vars.b=42` then `renderOnce(vars.a)=42`. Note that if `vars.b={{ vars.c }}`, `renderOnce(vars.a)={{ vars.c }}`. - -The syntax for the `renderOnce()` function is as follows: - -```yaml -{{ renderOnce(expression_string) }} -``` - -## secret - -The `secret()` function is used to retrieve a secret from a secret backend based on the key provided as input to that function. - -Here is an example flow that retrieves the Personal Access Token secret stored using the secret key `GITHUB_ACCESS_TOKEN`: - - -```yaml -id: secret -namespace: company.team - -tasks: - - id: githubPAT - type: io.kestra.plugin.core.log.Log - message: "{{ secret('GITHUB_ACCESS_TOKEN') }}" -``` - -The `secret('key')` function will lookup up the configured secret manager backend for a secret value using the key `GITHUB_ACCESS_TOKEN`. If the key is missing, an exception will be raised. The example flow shown above will look up the secret value by key and will log the output with the secret value. - -::alert{type="warning"} -The purpose of this example is to show how to use secrets in your flows. In practice, **you should avoid logging sensitive information**. -:: - -There are some differences between the secret management backend in the Open-Source and Enterprise editions. By default, Kestra provides a secret management backend based on environment variables. Each environment variable starting with `SECRET_` will be available as a secret, and its value must be base64-encoded. - -The above example will: -1. retrieve the secret `GITHUB_ACCESS_TOKEN` from an environment variable `SECRET_GITHUB_ACCESS_TOKEN` -2. base64-decode it at runtime. - - diff --git a/content/docs/expressions/05.operator.md b/content/docs/expressions/05.operator.md deleted file mode 100644 index 9c10183203..0000000000 --- a/content/docs/expressions/05.operator.md +++ /dev/null @@ -1,161 +0,0 @@ ---- -title: Operator -icon: /docs/icons/expression.svg ---- - -Operators are used to perform logical operations within templated expressions such as comparing values and performing arithmetic operations. - -## Comparison operators - -Pebble provides the following comparison operators: `==`, `!=`, `<`, `>`, `<=`, `>=`. All of them except for `==` -are equivalent to their Java counterparts. The `==` operator uses `Java.util.Objects.equals(a, b)` behind the -scenes to perform null safe value comparisons. - -> `equals` is an alias for `==` - -```twig -{% if user.name equals "Mitchell" %} - ... -{% endif %} -``` - -## concat - -The `concat` operator can be used to concatenate 2 strings: - -```twig -{{ "apple" ~ "pear" ~ "banana" }} -{# results in: 'applepearbanana' #} -``` - -## contains - -The `contains` operator can be used to determine if a collection, map, or array contains a particular item. -```twig -{% if ["apple", "pear", "banana"] contains "apple" %} - ... -{% endif %} -``` - -When using maps, the contains operator checks for an existing key. - -```twig -{% if {"apple":"red", "banana":"yellow"} contains "banana" %} - ... -{% endif %} -``` - -The operator can be used to look for multiple items at once: - -```twig -{% if ["apple", "pear", "banana", "peach"] contains ["apple", "peach"] %} - ... -{% endif %} -``` - -## is - -The `is` operator will apply a test to a variable which will return a boolean. - -```twig -{% if 2 is even %} - ... -{% endif %} -``` -The result can be negated using the `not` operator: - -```twig -{% if 3 is not even %} - ... -{% endif %} -``` - -## logic - -The `and` operator and the `or` operator are available to join boolean expressions. - -```twig -{% if 2 is even and 3 is odd %} - ... -{% endif %} -``` -The `not` operator is available to negate a boolean expression. -```twig -{% if 3 is not even %} - ... -{% endif %} -``` - -Parenthesis can be used to group expressions to ensure a desired precedence. - -```twig -{% if (3 is not even) and (2 is odd or 3 is even) %} - ... -{% endif %} -``` - -## math - -All the regular math operators are available for use. Order of operations applies. -```twig -{{ 2 + 2 / ( 10 % 3 ) * (8 - 1) }} -``` - -The following operators are supported: - -- `+`: Adds two numbers together (the operands are cast to numbers). `{{ -1 + 1 }}` is 2. -- `-`: Subtracts the second number from the first one. `{{ 3 - 2 }}` is 1. -- `/`: Divides two numbers. The returned value will be a floating point number. `{{ 1 / 2 }}` is `{{ 0.5 }}`. -- `%`: Calculates the remainder of an integer division. `{{ 11 % 7 }}` is 4. -- `*`: Multiplies the left operand with the right one. `{{ 2 * 2 }}` would return 4. - - -The result can be negated using the [not](#not) operator. - -## not - -The `not` operator is used in conjunction with [is](#is) will negate the test. - -```twig -{% if 3 is not even %} - ... -{% endif %} -``` - -## null-coalescing - -Kestra supports the null-coalescing operator that allows testing if variables are defined. - -The `??` operator will return the first defined, not-null value in the list. - -The `???` operator will return the right-hand side of the expression only if the left-hand side is undefined. - -::alert{type="info"} -TL;DR for the null-coalescing operator: -- `X ?? Y` will return `Y` if `X` is null or undefined -- `X ??? Y` will return `Y` only if `X` is undefined -:: - - -```twig -{% set baz = "baz" %} -{{ foo ?? bar ?? baz }} - -{# results in: 'baz' #} - -{{ foo ?? bar ?? raise }} -{# results: an exception because none of the 3 vars is defined #} -``` - -::alert{type="info"} -For more details on using the null-coalescing operator, see the [Handling null and undefined values](../15.how-to-guides/null-values.md) guide. -:: - -## ternary-operator - -Pebble supports the use of the conditional operator (often named the ternary operator). -```twig -{{ foo == null ? bar : baz }} -``` - diff --git a/content/docs/expressions/06.tag.md b/content/docs/expressions/06.tag.md deleted file mode 100644 index 8d19c25ca2..0000000000 --- a/content/docs/expressions/06.tag.md +++ /dev/null @@ -1,183 +0,0 @@ ---- -title: Tag -icon: /docs/icons/expression.svg ---- - -Tags are used to control the flow of the template. They are enclosed in `{% %}`. - -Each section below represents a built-in tag. - - -## block - -The `block` tag is used to render the contents of a block more than once. - -In the following example we create a block with the name 'header': - -```twig -{% block header %} - Introduction -{% endblock header %} -``` - -Then the `block` tag can be used with the [block](./04.function.md#block) function - -```twig -{{ block("post") }} -``` - -## filter - -The `filter` tag allows you to apply a filter to a large chunk of template. - -```twig -{% filter upper %} - hello -{% endfilter %} - -{# output: 'HELLO' #} -``` - -Multiple filters can be chained together. -```twig -{% filter upper | lower | title %} - hello -{% endfilter %} - -{# output: 'Hello' #} -``` - -## for - -The `for` tag is used to iterate through primitive arrays or anything that implements the `java.lang.Iterable` -interface, as well as maps. - -```twig -{% for user in users %}~ - {{ user.name }} lives in {{ user.city }}. -{% endfor %} -``` - -While inside the loop, Pebble provides a couple of special variables to help you out: -- `loop.index` - a zero-based index that increments with every iteration. -- `loop.length` - the size of the object we are iterating over. -- `loop.first` - True if first iteration -- `loop.last` - True if last iteration -- `loop.revindex` - The number of iterations from the end of the loop - -```twig -{% for user in users %} - {{ loop.index }} - {{ user.id }} -{% endfor %} -``` - -The `for` tag also provides a convenient way to check if the iterable object is empty with the included `else` tag. - -```twig -{% for user in users %} - {{ loop.index }} - {{ user.id }} -{% else %} - There are no users to display. -{% endfor %} -``` - -Iterating over maps can be done like so: - -```twig -{% for entry in map %} - {{ entry.key }} - {{ entry.value }} -{% endfor %} -``` - -## if - -The `if` tag allows you to designate a chunk of content as conditional depending on the result of an expression - -```twig -{% if users is empty %} - There are no users. -{% elseif users.length == 1 %} - There is only one user. -{% else %} - There are many users. -{% endif %} -``` - -The expression used in the `if` statement often makes use of the [is](./05.operator.md#is) operator. - -**# Supported conditions** - -`If` tag currently supports the following expression - -| Value | Boolean expression | -| --- | --- | -| boolean | boolean value | -| Empty string | false | -| Non empty string | true | -| numeric zero | false | -| numeric different than zero | true | - -## macro - -The `macro` tag allows you to create a chunk of reusable and dynamic content. The macro can be called -multiple times in the current template. - -It doesn't matter where in the current template you define a macro, i.e. whether it's before or after you call it. -Here is an example of how to define a macro: - -```twig -{% macro input(type="text", name, value) %} - type="{{ type }}", name="{{ name }}", value="{{ value }}" -{% endmacro %} -``` - -And now the macro can be called numerous times throughout the template, like so: - -```twig -{{ input(name="country") }} -{# will output: type="text", name="country", value="" #} -``` - -A macro does not have access to the same variables that the rest of the template has access to. -A macro can only work with the variables provided as arguments. - -**# Access to the global context** -You can pass the whole context as an argument by using the special `_context` variable if you need to access -variables outside of the macro scope: - -```twig -{% set foo = 'bar' %} - -{{ test(_context) }} -{% macro test(_context) %} - {{ _context.foo }} -{% endmacro %} - -{# will output: bar #} -``` - -## raw - -The `raw` tag allows you to write a block of Pebble syntax that won't be parsed. - -```twig -{% raw %}{{ user.name }}{% endraw %} -``` - -```twig -{% raw %} - {% for user in users %} - {{ user.name }} - {% endfor %} -{% endraw %} -``` - -## set - -The `set` tag allows you to define a variable in the current context, whether it currently exists or not. - -```twig -{% set header = "Test Page" %} - -{{ header }} -``` diff --git a/content/docs/expressions/07.test.md b/content/docs/expressions/07.test.md deleted file mode 100644 index bcf62a51e7..0000000000 --- a/content/docs/expressions/07.test.md +++ /dev/null @@ -1,91 +0,0 @@ ---- -title: Test -icon: /docs/icons/expression.svg ---- - -Tests are used to perform logical operations within templated expressions such as checking if a variable is defined or if a variable is empty. - -Each section below represents a built-in test. - -## defined - -The `defined` test checks if a variable is defined. - -```twig -{% if missing is not defined %} - ... -{% endif %} -``` - -## empty - -The `empty` test checks if a variable is empty. A variable is empty if it is null, an empty string, an empty collection, or an empty map. - -```twig -{% if user.email is empty %} - ... -{% endif %} -``` - - -## even - -The `even` test checks if an integer is even. - -```twig -{% if 2 is even %} - ... -{% endif %} -``` - -## iterable - -The `iterable` test checks if a variable implements `java.lang.Iterable`. - -```twig -{% if users is iterable %} - {% for user in users %} - ... - {% endfor %} -{% endif %} -``` - -## json - -The `json` test checks if a variable is valid json string - -```twig -{% if '{"test": 1}' is json %} - ... -{% endif %} -``` - -## map - -The `map` test checks if a variable is an instance of a map. - -```twig -{% if {"apple":"red", "banana":"yellow"} is map %} - ... -{% endif %} -``` - -## null - -The `null` test checks if a variable is null. - -```twig -{% if user.email is null %} - ... -{% endif %} -``` - -## odd - -The `odd` test checks if an integer is odd. -```twig -{% if 3 is odd %} - ... -{% endif %} -``` - diff --git a/content/docs/expressions/08.deprecated-handlebars.md b/content/docs/expressions/08.deprecated-handlebars.md deleted file mode 100644 index 3d8ddc5a2e..0000000000 --- a/content/docs/expressions/08.deprecated-handlebars.md +++ /dev/null @@ -1,1010 +0,0 @@ ---- -title: Deprecated handlebars -icon: /docs/icons/expression.svg ---- - -Handlebars are deprecated and superseded by Pebble. These functions will be removed soon and are disabled by default. - -## boolean - -**`eq`: Equality** - -Test if two elements are equals. - -> Render `yes` or `no`: -```handlebars - {{ #eq a b }} - yes - {{ else }} - no - {{ /eq }} -``` - -> Render `true` or `false`: - -```handlebars - {{ eq a b }} -``` - -> Render `yes` or `no`: - -```handlebars - {{ eq a b yes='yes' no='no' }} -``` - -**`neq`: Not equality** - -Test if two elements are NOT equals. - -> Render `yes` or `no`: - -```handlebars - {{ #neq a b }} - yes - {{ else }} - no - {{ /neq }} -``` - -> Render `true` or `false`: - -```handlebars - {{ neq a b }} -``` - -> Render `yes` or `no`: - -```handlebars - {{ neq a b yes='yes' no='no' }} -``` - -**`gt`: Greater operator** - -Greater operator (arguments must be [Comparable](https://docs.oracle.com/javase/8/docs/api/java/lang/Comparable.html) elements). - -> Render `yes` or `no`: - -```handlebars - {{ #gt a b }} - yes - {{ else }} - no - {{ /gt }} -``` - -> Render `true` or `false`: - -```handlebars - {{ gt a b }} -``` - -> Render `yes` or `no`: -```handlebars - {{ gte a b yes='yes' no='no' }} -``` - -**`gte`: Greater or equal operator** - -Greater or equal operator (arguments must be [Comparable](https://docs.oracle.com/javase/8/docs/api/java/lang/Comparable.html) elements). - -> Render `yes` or `no`: - -```handlebars - {{ #gte a b }} - yes - {{ else }} - no - {{ /gte }} -``` - -> Render `true` or `false`: - -```handlebars - {{ gte a b }} -``` - -> Render `yes` or `no`: - -```handlebars - {{ gte a b yes='yes' no='no' }} -``` - -**`lt`: Less operator** - -Less than operator (arguments must be [Comparable](https://docs.oracle.com/javase/8/docs/api/java/lang/Comparable.html) elements). - -> Render `yes` or `no`: - -```handlebars - {{ #lt a b }} - yes - {{ else }} - no - {{ /lt }} -``` - -> Render `true` or `false`: -```handlebars - {{ lt a b }} -``` - -> Render `yes` or `no`: - -```handlebars - {{ lt a b yes='yes' no='no' }} -``` - -**`lte`: Less or equal operator** - -Less than operator (arguments must be [Comparable](https://docs.oracle.com/javase/8/docs/api/java/lang/Comparable.html) elements. - -> Render `yes` or `no`: - -```handlebars - {{ #lte a b }} - yes - {{ else }} - no - {{ /lte }} -``` - -> Render `true` or `false`: - -```handlebars - {{ lte a b }} -``` - -> Render `yes` or `no`: - -```handlebars - {{ lte a b yes='yes' no='no' }} -``` - -**`and`: And operator** - -And operator. This is a boolean operation. - -Truthiness of arguments is determined by [isEmpty](https://docs.oracle.com/javase/7/docs/api/java/lang/String.html#isEmpty()), so this -helper can be used with non-boolean values. - -Multiple arguments are supported too: - -```handlebars - {{ #and a b c d }} - yes - {{ else }} - no - {{ /and }} -``` - -> Render `yes` or `no`: - -```handlebars - {{ #and a b }} - yes - {{ else}} - no - {{ /and }} -``` - - -> Render `true` or `false`: - -```handlebars - {{ and a b }} -``` - -> Render `yes` or `no`: - -```handlebars - {{ and a b yes='yes' no='no' }} -``` - -**`or`: Or operator** - -Or operator. This is a boolean operation - -Truthiness of arguments is determined by [isEmpty](https://docs.oracle.com/javase/7/docs/api/java/lang/String.html#isEmpty()), so this -helper can be used with non-boolean values. - -Multiple arguments are supported too: - -```handlebars - {{ #or a b c d }} - yes - {{ else }} - no - {{ /or }} -``` - - -> Render `yes` or `no`: -```handlebars - {{ #or a b }} - yes - {{ else }} - no - {{ /or }} -``` - -> Render `true` or `false`: -```handlebars - {{ or a b }} -``` - -> Render `yes` or `no`: -```handlebars - {{ or a b yes='yes' no='no' }} -``` - -**`not`: Not operator** - -Truthiness of arguments is determined by [isEmpty](https://docs.oracle.com/javase/7/docs/api/java/lang/String.html#isEmpty()), so this -helper can be used with non-boolean values. - - -> Render `yes` or `no`: -```handlebars - {{ #not a }} - yes - {{ else }} - no - {{ /not }} -``` - -> Render `true` or `false`: -```handlebars - {{ not a }} -``` - -> Render `y` or `n`: -```handlebars - {{ not a yes='yes' no='no' }} -``` - -**`cmp`: Compare operator** - -Compare to object as [Comparable](https://docs.oracle.com/javase/8/docs/api/java/lang/Comparable.html)s. - - -> Renders 1 if a > b, 0 if a == b -1 if a < b -```handlebars - {{ cmp a b }} -``` - -**`isNull`: Compare operator** - -Test if one element is null. - -```handlebars - {{ isNull a }} -``` - -**`isNotNull`: Compare operator** - -Test if one element is not null. - -```handlebars - {{ isNotNull a }} -``` - ---- - -## date - - -**`dateFormat`: Date format** - -```handlebars -{{ dateFormat date ['format'] [format='format'][tz=timeZone | timeZoneId] }} -``` - - -**## Arguments:** -- `format`: Format parameters is one of: - - `full`: Sunday, September 8, 2013 at 4:19:12 PM Central European Summer Time - - `long`: September 8, 2013 at 4:19:12 PM CEST - - `medium`: Sep 8, 2013, 4:19:12 PM - - `short`: 9/8/13, 4:19 PM - - `iso`: 2013-09-08T16:19:12.000000+02:00 - - `iso_sec`: 2013-09-08T16:19:12+02:00 - - `sql`: 2013-09-08 16:19:12.000000 - - `sql_seq`: 2013-09-08 16:19:12 - - `iso_date_time`: 2013-09-08T16:19:12+02:00[Europe/Paris] - - `iso_date`: 2013-09-08+02:00 - - `iso_time`: 16:19:12+02:00 - - `iso_local_date`: 2013-09-08 - - `iso_instant`: 2013-09-08T14:19:12Z - - `iso_local_date_time`: 2013-09-08T16:19:12 - - `iso_local_time`: 16:19:12 - - `iso_offset_time`: 16:19:12+02:00 - - `iso_ordinal_date`: 2013-251+02:00 - - `iso_week_date`: 2013-W36-7+02:00 - - `iso_zoned_date_time`: 2013-09-08T16:19:12+02:00[Europe/Paris] - - `rfc_1123_date_time`: Sun, 8 Sep 2013 16:19:12 +0200 - - `pattern`: a date pattern. - - Otherwise, the default formatter `iso` will be used. The format option can be specified as a parameter or hash (a.k.a named parameter). - - You can pass the any format from [SimpleDateFormat](https://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html) -- `timezeome`: with the format `Europe/Paris` - -**`now`: Current date** - -```handlebars - {{ now ['format'] [tz=timeZone|timeZoneId] }} -``` - -**## Arguments:** -- `format`: Same format as `dateFormat` - - `timezone`: with the format `Europe/Paris` - -**`timestamp`: Current second timestamp** - -```handlebars - {{ timestamp }} -``` - - -**`nanotimestamp`: Current nano timestamp** - -```handlebars - {{ nanotimestamp }} -``` - - -**`microtimestamp`: Current micro timestamp** - -```handlebars - {{ microtimestamp }} -``` - - - -**`dateAdd`: Add some units to date** - -```handlebars - {{ dateAdd yourDate quantity "unit" [format="format"] [tz=timeZone|timeZoneId] }} - {{ dateAdd yourDate -1 "DAYS" }} -``` -- `quantity`: an integer value positive or negative -- `format`: Format parameters is one of: - - `NANOS` - - `MICROS` - - `MILLIS` - - `SECONDS` - - `MINUTES` - - `HOURS` - - `HALF_DAYS` - - `DAYS` - - `WEEKS` - - `MONTHS` - - `YEARS` - - `DECADES` - - `CENTURIES` - - `MILLENNIA` - - `ERAS` - ---- - -## index - -::alert{type="warning"} -Handlebars variables are deprecated and superseded by Pebble. These functions will be removed soon and are disabled by default. -:: - -Variables are specific fields for tasks. They use the power of [handlebars](https://handlebarsjs.com/guide/) with Kestra's special context system, allowing powerful task composition. - -Variables can use variable information registered/existing in the execution context. The context is data injected in Variables and from different sources: - -**Functions** - -Sometimes, you need to change the format of variables. To do this, you can use some of the following functions: - - ---- - -## iterations - -**For each** - -You can iterate over a list using the built-in each helper. Inside the block, you can use `this` to reference the element being iterated over. `contextualListVariable` is an iterable item on which the mydata property is displayed for all entries. - -The `@index` is a special variable available in the each loop context which value is the current index of the element being iterated. There are agic* variables like @index in a each context. The following ones are also available: `@key` `@index` `@first` `@last` `@odd` `@even` - -See handlebars documentation for more about this topic. - -```handlebars -{{ #each contextualListVariable }} - {{ this.mydata }} {{ @index }} -{{ /each }} -``` - -will produce - -``` -one 0 -two 1 -three 2 -django! 3 -``` - -for the following data sample when - -```javascript -contextualListVariable = [ - {"mydata": "one"}, - {"mydata": "two"}, - {"mydata": "three"}, - {"mydata": "django!"}, -] -``` - -If the contextual 'this' is not convenient to use the data as you wish, it is possible to alias it as shown below: - -```handlebars -{{ #each iterableVariable as | myItem | }} - {{myItem.mydata}} -{{ /each }} -``` - ---- - -## json - -**`json` Convert an object to json** - -Convert an object to is JSON representation - -```handlebars -{{ json output['task-id'] }} -``` - -Example, if the current context is: -```json -{ - "outputs": { - "task1": { - "value": 1, - "text": "awesome1" - }, - "task2": { - "value": 2, - "text": "awesome2" - } - } -} -``` - -the output of `{{ json outputs.task2}}` will be `{"value":2,"text":"awesome2"}`. - -**`jq` Transform vars with JQ** - -Apply the [JQ expression](https://stedolan.github.io/jq/) to a variables. - -```handlebars -{{ jq vars jqExpr [first=false] }} -``` - -`first` mean to always fetch the first element, by default jq return an array of results - -::alert{type="warning"} -Internally, [Jackson JQ](https://github.com/eiiches/jackson-jq) is used and support only a large subset of official JQ. -:: - - -Example, if the current context is: - -```json -{ - "outputs": { - "task1": { - "value": 1, - "text": "awesome1" - }, - "task2": { - "value": 2, - "text": "awesome2" - } - } -} -``` - -```handlebars -{{ jq outputs .task1.value true }} -``` - -the output will be `1`. - - ---- - -## number - -**`numberFormat`: Format a number** -```handlebars - {{ numberFormat number ["format"] [locale=default] }} -``` - -Arguments: - - `format`: Format parameters is one of: - - "integer": the integer number format - - "percent": the percent number format - - "currency": the decimal number format - - "pattern": a decimal pattern. - - Otherwise, the default formatter will be used. - -More options: - -- groupingUsed: Set whether or not grouping will be used in this format. -- maximumFractionDigits: Sets the maximum number of digits allowed in the fraction portion of -a number. -- maximumIntegerDigits: Sets the maximum number of digits allowed in the integer portion of a -number -- minimumFractionDigits: Sets the minimum number of digits allowed in the fraction portion of -a number -- minimumIntegerDigits: Sets the minimum number of digits allowed in the integer portion of a -number. -- parseIntegerOnly: Sets whether or not numbers should be parsed as integers only. -- roundingMode: Sets the {@link java.math.RoundingMode} used in this NumberFormat. - ---- - -## string - -**`capitalizeFirst`: Capitalizes the first character of the value.** - -If the value is "kestra is cool !", the output will be "Kestra is cool !". - -```handlebars -{{ capitalizeFirst value }} -``` - -**`center`: Centers the value in a field of a given width.** - -If the value is "Handlebars.java", the output will be " Handlebars.java ". - -```handlebars -{{ center value size=19 [pad="char"] }} -``` - -**## Arguments:** -- `size` -- `pad` - - -**`cut`: Removes all values of arg from the given string.** - -If the value is "String with spaces", the output will be "Stringwithspaces". - -```handlebars -{{ cut value [" "] }} -``` - - - - -**`defaultIfEmpty`: Default if empty** - -If the value evaluates to False, it will use the given default. Otherwise, it uses the -value. If value is "" (an empty string), the output will be nothing. - - -```handlebars -{{ defaultIfEmpty value ["nothing"] }} -``` - - -**`join`: Join string** - -Joins an array, iterator or an iterable with a string. - -```handlebars -{{ join value join [prefix=""] [suffix=""] }} -``` - -**## Arguments:** -- `join` -- `prefix` -- `suffix` - -> If value is the list ['a', 'b', 'c'], the output will be the string "a // b // c". -```handlebars -{{ join value " // " [prefix=""] [suffix=""] }} -``` - -> Join the "a", "b", "c", the output will be the string "a // b // c". -```handlebars -{{ join "a" "b" "c" " // " [prefix=""] [suffix=""] }} -``` - - - -**`ljust`: Left-aligns the value in a field of a given width.** - -If the value is Handlebars.java, the output will be "Handlebars.java ". - - -```handlebars -{{ ljust value 20 [pad=" "] }} -``` - -**## Arguments:** -- `field size` - - -**`rjust`: Right-aligns the value in a field of a given width.** - -If the value is Handlebars.java, the output will be " Handlebars.java". - -```handlebars -{{ rjust value 20 [pad=" "] }} -``` - -**## Arguments:** -- `field size` -- `pad` - - - -**`substring` Substring** - -Returns a new `CharSequence` that is a subsequence of this sequence. -The subsequence starts with the `char` value at the specified index and -ends with the `char` value at nd - 1* - -```handlebars -{{substring value start end }} -``` - -**## Arguments:** -- `start offset` -- `end offset` - -For example: - -> If the value is Handlebars.java, the output will be "java". -```handlebars -{{ substring value 11 }} -``` - -> If the value is Handlebars.java, the output will be "Handlebars". -```handlebars -{{ substring value 0 10 }} -``` - -**`lower`: Converts a string into all lowercase.** - -If the value is 'Still MAD At Yoko', the output will be 'still mad at yoko'. - -```handlebars -{{ lower value}} -``` - - -**`upper` Converts a string into all uppercase.** - -If the value is 'Hello', the output will be 'HELLO'. - -```handlebars -{{ upper value }} -``` - - -**`slugify` Converts to lowercase** - -This removes non-word characters (alphanumerics and underscores) and converts spaces to hyphens. It also strips leading and trailing whitespace. -If the value is "Joel is a slug", the output will be "joel-is-a-slug". - -```handlebars -{{ slugify value }} -``` - - - -**`stringFormat`: Formats the variable** - -According to the argument, a string formatting specifier. -If the value is "Hello %s" "handlebars.java", the output will be "Hello handlebars.java". - -```handlebars -{{stringFormat string param0 param1 ... paramN}} -``` - -**## Arguments:** -- `format` -- `paramN` - - - - -**`stripTags`: Strips all [X]HTML tags.** - -```handlebars -{{ stripTags value }} -``` - -**`capitalize`: Capitalizes all the whitespace separated words in a String.** - -If the value is "my first post", the output will be "My First Post". - -```handlebars -{{ capitalize value [fully=false] }} -``` - -Arguments: -- `fully` - - - -**`abbreviate`: Truncates a string** - -The string will be truncated if it is longer than the specified number of characters. -Truncated strings will end with a translatable ellipsis sequence ("..."). -If value is "Handlebars rocks", the output will be "Handlebars...". - - -```handlebars -{{ abbreviate value 13 }} -``` - -**## Arguments:** -- Number of characters to truncate to - - - -**`wordWrap`: Wraps words** - -This wraps the sentence at a specified line length. If value is Joel is a slug, the output would be: `Joel\nis a\nslug` - - -```handlebars -{{ wordWrap value 5 }} -``` - -**## Arguments:** -- the number of characters at which to wrap the text - - - -**`replace` Replaces** - -Each substring of this string that matches the literal target sequence with the specified literal replacement sequence. -If value is "Handlebars ...", the output will be "Handlebars rocks". - -```handlebars -{{ replace value "..." "rocks" }} -``` - - -**`yesno`: Boolean to yes / no** - -For true, false and (optionally) null, to the strings "yes", "no", "maybe". - -**## Arguments:** - - `yes` - - `no` - - `maybe` - -```handlebars -{{ yesno value [yes="yes"] [no="no"] maybe=["maybe"] }} -``` - ---- - -## use - -Here you will find some examples to illustrate the available variables, and how to get the value you need. - -Here is a typical payload for variables: - -```yaml -globals: - my-global-string: string - my-global-int: 1 - my-global-bool: true - -task: - id: float - type: io.kestra.plugin.core.debug.Return - -taskrun: - id: 5vPQJxRGCgJJ4mubuIJOUf - startDate: 2020-12-18T12:46:36.018869Z - attemptsCount: 0 - value: value2 - -parent: - taskrun: - value: valueA - outputs: - int: 1 - -parents: - - taskrun: - value: valueA - outputs: - int: 1 - - taskrun: - value: valueB - outputs: - int: 2 - -flow: - id: inputs - namespace: company.team - -execution: - id: 42mXSJ1MRCdEhpbGNPqeES - startDate: 2020-12-18T12:45:28.489187Z - -outputs: - my-task-id-1: # standard task outputs - value: output-string - my-task-id-2: # standard task outputs - value: 42 - my-each-task-id: # dynamic task (each) - value1: # outputs for value1 - value: here is value1 - value2: # outputs for value2 - value: here is value2 - -inputs: - file: kestra:///org/kestra/tests/inputs/executions/42mXSJ1MRCdEhpbGNPqeES/inputs/file/application.yml - string: myString - instant: 2019-10-06T18:27:49Z -``` - - -**Common variables** -As you can see, there are a lot of common variables that can be used in your flow, some of the most common examples are: `{{ execution.id }}`, `{{ execution.startDate }}` that allows you to change a file name or SQL query, for example. - -**Input variables** -Input variables are simple to access with `{{ execution.NAME }}`, where `NAME` is the name of the declared in your flow. The data will be dependent on the `type` of the inputs. -One special case for input variables is the `FILE` type, where the file is prepended by `kestra://`. This means the file is inside the internal Kestra storage. Most tasks will take this kind of URI as a property and will provide the same property output. This type of input variable allows the full file generated by one task to be used in another task. - -**Outputs variables** -One of Kestra's most important abilities is to use all outputs from previous tasks in the next one. - -**Without dynamic tasks (Each)** -This is the simplest and most common way to use outputs in the next task. In order to fetch a variable, just use `{{ outputs.ID.NAME }}` where: -* `ID` is the task id -* `NAME` is the name of the output. Each task type can have any outputs that are documented on the part outputs of their docs. For example, Bash task can have `{{ outputs.ID.exitCode }}`, `{{ outputs.ID.outputFiles }}`, `{{ outputs.ID.stdErrLineCount }}`, etc... - -**With dynamic tasks (Each)** -This option is more complicated since Kestra will change the way the outputs are generated, since there can be multiple tasks with the same id, you will need to use `{{ outputs.ID.VALUE.NAME }}`. - -Most of the time, using Dynamic Tasks, you will need to fetch the current value of the iteration. This is done easily with `{{ taskrun.value }}`. - -But what if a more complex flow is built, for example, with each containing 1 task (`t1`) to download a file (based on each value), and a second one (`t2`) that needs the output of `t1`. Such a flow would look something like this: - -```yaml -id: each-sequential-nested -namespace: company.team - -tasks: - - id: each - type: io.kestra.plugin.core.flow.EachSequential - value: '["s1", "s2", "s3"]' - tasks: - - id: t1 - type: io.kestra.plugin.core.debug.Return - format: "{{ task.id }} > {{ taskrun.value }}" - - id: t2 - type: io.kestra.plugin.core.debug.Return - format: "{{ task.id }} > {{ (get outputs.t1 taskrun.value).value }} > {{ taskrun.startDate }}" - - id: end - type: io.kestra.plugin.core.debug.Return - format: "{{ task.id }}" -``` - -In this case, you would need to use `{{ (get outputs.t1 taskrun.value).value }}`, which means give me from `outputs.t1` the index results from `taskrun.value`. - -**With Flowable Task nested.** -If you have many Flowable Tasks, it can be complex to use the `get` function, and moreover, the `taskrun.value` is only available during the direct task from each. If you have any Flowable Tasks after, the `taskrun.value` of the first iteration will be lost (or overwritten). To deal with this, we have included the `parent` & `parents` vars. - -This is illustrated in the flow below: - -```yaml -id: each-switch -namespace: company.team - -tasks: - - id: t1 - type: io.kestra.plugin.core.debug.Return - format: "{{ task.id }} > {{ taskrun.startDate }}" - - id: 2_each - type: io.kestra.plugin.core.flow.EachSequential - value: '["a", "b"]' - tasks: - # Switch - - id: 2-1_switch - type: io.kestra.plugin.core.flow.Switch - value: "{{ taskrun.value }}" - cases: - a: - - id: 2-1_switch-letter-a - type: io.kestra.plugin.core.debug.Return - format: "{{ task.id }}" - b: - - id: 2-1_switch-letter-b - type: io.kestra.plugin.core.debug.Return - format: "{{ task.id }}" - - - id: 2-1_each - type: io.kestra.plugin.core.flow.EachSequential - value: '["1", "2"]' - tasks: - - id: 2-1-1_switch - type: io.kestra.plugin.core.flow.Switch - value: "{{ taskrun.value }}" - cases: - 1: - - id: 2-1-1_switch-number-1 - type: io.kestra.plugin.core.debug.Return - format: "{{ parents[0].taskrun.value }}" - 2: - - id: 2-1-1_switch-number-2 - type: io.kestra.plugin.core.debug.Return - format: "{{ parents[0].taskrun.value }} {{ parents[1].taskrun.value }}" - - id: 2_end - type: io.kestra.plugin.core.debug.Return - format: "{{ task.id }} > {{ taskrun.startDate }}" - -``` - -As you can see, the `parent` will give direct access to the first parent output and the value of the current one, while the `parents[INDEX]` lets go you 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` - ---- - -## vars - -**`firstDefined` First defined variables** - -Return the first defined variables or throw an exception if no variables are defined. - -```handlebars -{{ firstDefined outputs.task1.uri outputs.task2.uri }} -``` - -**`eval` Evaluate a handlebars expression** - -Evaluate a handlebars expression at runtime based on the whole context. - -Mostly useful for [Lookup in current child's tasks tree](./02.expression-usage.md#parent-tasks-with-flowable-tasks) and dynamic tasks. - - -```handlebars -{{ eval 'outputs.first.[{{taskrun.value}}].value' }} -``` - -**`firstDefinedEval` First defined evaluation** - -First defined evaluates a handlebars expression at runtime based on the whole context or throws an exception if no variables are defined. - -Mostly useful for [Lookup in current child's tasks tree](./02.expression-usage.md#parent-tasks-with-flowable-tasks) and dynamic tasks. - - -```handlebars -{{ firstDefined 'outputs.first.value' 'outputs.first.[{{taskrun.value}}].value' }} -``` - -**`get` get an element for an array or map by key** -```handlebars - {{ get object ["key"] }} -``` - -* get on `object` type map, the key at `key` -* get on `object` type array, the index at `key` - -Mostly useful for [Lookup in current child's tasks tree](./02.expression-usage.md#parent-tasks-with-flowable-tasks) and dynamic tasks. - -```handlebars -{{ get outputs 'first' }} -``` - ---- diff --git a/content/docs/expressions/index.md b/content/docs/expressions/index.md index 10b6ed15e2..be174271ac 100644 --- a/content/docs/expressions/index.md +++ b/content/docs/expressions/index.md @@ -3,19 +3,2122 @@ title: Expressions icon: /docs/icons/expression.svg --- -Expressions to dynamically render various flow and task properties. +Expressions & Context Variables -Kestra's expressions use [Pebble Templating](https://pebbletemplates.io/) along with flow's execution context to render various flow and task properties. +## Overview -To dynamically set values for your tasks and flows, you can add an expression by using `{{ expression_name }}` syntax. Dynamic expressions can be used on each task property that is documented as **dynamic**. +Kestra's expressions combine the [Pebble templating engine](../05.concepts/06.pebble.md) with the execution context to dynamically render flow properties. This page lists available expressions and explains how to use them in your flows. -Kestra uses an [integrated templating engine](../05.concepts/06.pebble.md) to dynamically render variables, inputs and outputs within the execution context. +## Using Expressions -Flows, tasks, executions, triggers, and schedules have default expressions. For example, `{{ flow.id }}` allows accessing the identifier of a flow within an execution. +To dynamically set values in your flows, wrap an expression in curly braces, e.g. `{{ your_expression }}`. -Flow inputs can be accessed using `{{ inputs.myinput }}`, and task's output attributes are available as `{{ outputs.task_id.output_attribute }}` expressions. +Flows, tasks, executions, triggers, and schedules come with built-in expressions. For example: +- `{{ flow.id }}` gives the flow's identifier within an execution +- `{{ inputs.myinput }}` retrieves an input value passed to the execution +- `{{ outputs.mytask.myoutput }}` fetches a task's output. -Most expressions are stored in the execution context. The execution context encompasses flow and execution, environment variables, global variables, plugin defaults, flow inputs, and task outputs. Expressions of a FILE-type are stored in Kestra's internal storage and fetched from there at execution time. +To debug expressions, use the **Debug Outputs** console as demonstrated in the video below: -::ChildCard +
+ +
+ +## Flow and Execution Expressions + +Flow and execution expressions let you use the execution context to set task properties. For example, you can reference `{{ execution.startDate }}` to include the execution's start date in a file name. + +Some expressions, such as `flow.id` or `flow.namespace`, access metadata stored in the execution context. Others, such as `FILE`-type inputs and outputs, pull data from Kestra's internal storage or environment variables. + +The execution context includes these variables: +- `flow` +- `execution` +- `inputs` +- `outputs` +- `labels` +- `tasks` +- `trigger` — available if at least one trigger is defined in the flow +- `vars` — available if variables are defined in the flow configuration +- `namespace` — available in EE when Variables are set in the Namespace configuration +- `envs` — environment variables +- `globals` — global variables. + +::alert{type="info"} +To see **all metadata** available in the **execution context**, use `{{ printContext() }}` in the Debug Outputs console. +![printContext](/docs/expressions/printContext.png) +:: + +--- + +### Default Execution Context Variables + +The following table lists the default execution context variables available in Kestra: + +| Parameter | Description | +|-------------------------------|------------------------------------------------------------------------------------------------------------------------------| +| `{{ flow.id }}` | Identifier of the flow. | +| `{{ flow.namespace }}` | Namespace of the flow. | +| `{{ flow.tenantId }}` | Identifier of the tenant (Enterprise Edition only). | +| `{{ flow.revision }}` | Revision number of the flow. | +| `{{ execution.id }}` | Unique ID of the execution. | +| `{{ execution.startDate }}` | Start date of the execution, which can be formatted using `{{ execution.startDate \| date("yyyy-MM-dd HH:mm:ss.SSSSSS") }}`. | +| `{{ execution.originalId }}` | Original execution ID, remains the same even during replay, retaining the first execution ID. | +| `{{ task.id }}` | ID of the current task. | +| `{{ task.type }}` | Type of the current task (Java fully qualified class name). | +| `{{ taskrun.id }}` | ID of the current task run. | +| `{{ taskrun.startDate }}` | Start date of the current task run. | +| `{{ taskrun.attemptsCount }}` | Number of attempts for the current task (includes retries or restarts). | +| `{{ taskrun.parentId }}` | Parent ID of the current task run. Available only for tasks nested under a Flowable task. | +| `{{ taskrun.value }}` | Value of the current task run. Available only for tasks wrapped in Flowable tasks. | +| `{{ parent.taskrun.value }}` | Value of the nearest parent task run. Available only for tasks nested under a Flowable task. | +| `{{ parent.outputs }}` | Outputs of the nearest parent task run. Available only for tasks nested under a Flowable task. | +| `{{ parents }}` | List of parent tasks. Available only for tasks nested under a Flowable task. | +| `{{ labels }}` | Execution labels accessible by keys, e.g. `{{ labels.myKey }}`. | + +#### Additional Variables for `Schedule` Trigger + +When the execution is triggered by a `Schedule`, the following variables are also available: + +| Parameter | Description | +|-----------------------|----------------------------------------| +| `{{ trigger.date }}` | Date of the current schedule. | +| `{{ trigger.next }}` | Date of the next schedule. | +| `{{ trigger.previous }}` | Date of the previous schedule. | + +#### Additional Variables for `Flow` Trigger + +When the execution is triggered by a `Flow`, the following variables are also available: + +| Parameter | Description | +|----------------------------|------------------------------------------------------------| +| `{{ trigger.executionId }}` | ID of the execution triggering the current flow. | +| `{{ trigger.namespace }}` | Namespace of the flow triggering the current flow. | +| `{{ trigger.flowId }}` | ID of the flow triggering the current flow. | +| `{{ trigger.flowRevision }}` | Revision of the flow triggering the current flow. | + +All expressions can be used with the Pebble template syntax `{{ expression }}`. For example: + +```yaml +id: expressions +namespace: company.team + +tasks: + - id: debug_expressions + type: io.kestra.plugin.core.debug.Return + format: | + taskId: {{ task.id }} + date: {{ execution.startDate | date("yyyy-MM-dd HH:mm:ss.SSSSSS") }} +``` + +::alert{type="info"} +Use the `date` filter to format the `execution.startDate` variable as `yyyy-MM-dd HH:mm:ss.SSSSSS`, e.g., `{{ execution.startDate | date("yyyy-MM-dd HH:mm:ss.SSSSSS") }}`. +:: + +--- + +### Environment Variables + +Kestra provides access to environment variables prefixed with `KESTRA_` by default, unless configured otherwise in the `variables` [configuration](../configuration/index.md). + +To use an environment variable, such as `KESTRA_FOO`, reference it as `{{ envs.foo }}`. The variable name is derived by removing the `KESTRA_` prefix and converting the remainder to **lowercase**. + +### Global Variables + +You can define global variables in Kestra's [configuration](../configuration/index.md) and access them using `{{ globals.foo }}`. + +### Flow Variables + +To avoid hardcoding values in tasks, you can declare variables at the flow level using the `variables` property. These variables can be accessed anywhere in the flow with the `vars.my_variable` syntax. For example: + +```yaml +id: flow_variables +namespace: company.team + +variables: + my_variable: "my_value" + +tasks: + - id: print_variable + type: io.kestra.plugin.core.debug.Return + format: "{{ vars.my_variable }}" +``` + +### Inputs + +Flow `inputs` can be referenced using the `inputs.inputName` syntax. For example: + +```yaml +id: render_inputs +namespace: company.team + +inputs: + - id: myInput + type: STRING + +tasks: + - id: myTask + type: io.kestra.plugin.core.debug.Return + format: "{{ inputs.myInput }}" +``` + +### Secrets + +You can retrieve secrets in your flow using the `secret()` function. Secrets are stored in a secure way and can be accessed as follows: + +```yaml +id: use_secret_in_flow +namespace: company.team + +tasks: + - id: myTask + type: io.kestra.plugin.core.debug.Return + format: "{{ secret('MY_SECRET') }}" +``` + +Secrets are supported in both the open-source version and [Enterprise Edition](/enterprise). For additional details, refer to the [Secrets](../05.concepts/04.secret.md) documentation. + +--- + +### Namespace Variables (EE) + +Namespace variables are key-value pairs defined in YAML configuration. They can be nested and referenced in flows using dot notation, e.g., `{{ namespace.myproject.myvariable }}`. To define namespace variables: +1. Navigate to `Namespaces` in the Kestra UI. +2. Select the namespace. +3. Add variables in the `Variables` tab. + +Namespace variables are scoped to the specific namespace and inherited by child namespaces. Reference these variables in your flow using the `namespace.your_variable` syntax. Example: + +```yaml +id: namespace_variables +namespace: company.team + +tasks: + - id: myTask + type: io.kestra.plugin.core.debug.Return + format: "{{ namespace.your_variable }}" +``` + +If a namespace variable contains Pebble expressions, such as `{{ secret('GITHUB_TOKEN') }}`, you need to use the `render` function to evaluate it. For example, assume the following variable is defined in the `Variables` tab: + +```yaml +github: + token: "{{ secret('GITHUB_TOKEN') }}" +``` + +To reference `github.token` in your flow, use `"{{ render(namespace.github.token) }}"`: + +```yaml +id: recursive_namespace_variables_rendering +namespace: company.team + +tasks: + - id: myTask + type: io.kestra.plugin.core.debug.Return + format: "{{ render(namespace.github.token) }}" +``` + +The `render()` function is required to parse namespace or flow variables containing Pebble expressions. Without it, the variable is treated as a string, and its expressions are not evaluated. + +--- + +### Outputs + +Task outputs can be accessed using `{{ outputs.taskId.outputAttribute }}`, where: +- `taskId` is the ID of the task +- `outputAttribute` is the attribute of the task's output. Each task emits specific output attributes — refer to task documentation for details. + +Example of passing data between tasks using `outputs`: + +```yaml +id: pass_data_between_tasks +namespace: company.team + +tasks: + - id: first + type: io.kestra.plugin.core.debug.Return + format: First output value + + - id: second-task + type: io.kestra.plugin.core.debug.Return + format: Second output value + + - id: print_both_outputs + type: io.kestra.plugin.core.log.Log + message: | + First: {{ outputs.first.value }} + Second: {{ outputs['second-task'].value }} +``` + +::alert{type="info"} +The `Return`-type task emits an output attribute called `value`. The `print_both_outputs` task demonstrates two ways to access outputs: +1. Dot notation: `{{ outputs.first.value }}` +2. Subscript notation: `{{ outputs['second-task'].value }}` — required for task IDs with special characters (e.g., hyphens). We recommend using `camelCase` or `snake_case` for task IDs to avoid this issue. +:: + +--- + +## Pebble Templating + +Pebble templating provides many ways to dynamically evaluate expressions. + +The example below demonstrates parsing Pebble expressions within `variables`, based on `inputs` and `trigger` values. The Null-Coalescing Operator `??` is used to select the first non-null value. + +### Parsing Complex Variables + +The workflow shown below defines two variables: +1. **`trigger_or_yesterday`:** + - Evaluates to `trigger.date` if the flow runs on a schedule. + - If no schedule is available, it defaults to yesterday’s date by subtracting one day from `execution.startDate`. + +2. **`input_or_yesterday`:** + - Evaluates to the `mydate` input if provided. + - If the input is absent, it defaults to yesterday’s date, calculated using `execution.startDate` minus one day with the `dateAdd` function. + +```yaml +id: render_complex_expressions +namespace: company.team + +inputs: + - id: mydate + type: DATETIME + required: false + +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) }}" +``` + +### Render Function and Null-Coalescing + +- **`render` Function:** the `render` function is required to evaluate variables containing Pebble expressions. Without it, variables will be treated as strings, and expressions inside them will not be evaluated. + +- **Null-Coalescing Operator (`??`):** this operator ensures that the first non-null value is selected, providing a fallback mechanism. + +Combining the `render()` function with the Null-Coalescing operator enables dynamic and flexible parsing of complex expressions. + +--- + +## Expression Usage + +This section summarizes the main syntax of filters, functions, and control structures available in Pebble templating. + +### Syntax Reference + +Pebble templates use two primary delimiters: +- `{{ ... }}`: outputs the result of an expression. Expressions can be simple variables or complex calculations. +- `{% ... %}`: controls the template’s flow, such as with `if` statements or `for` loops. + +To escape expressions or control structures, use the `raw` tag. This prevents Pebble from interpreting content within `{{ ... }}` or `{% ... %}`. + +Dot notation (`.`) is used to access nested attributes. For attributes with special characters, use square brackets: + +```twig +{{ foo.bar }} # Accesses 'bar' in 'foo' +{{ foo['foo-bar'] }} # Accesses 'foo-bar' in 'foo' +``` + +::alert{type="warning"} +For names with hyphens (`-`), use **subscript notation**: `{{ outputs.mytask.myoutput['foo-bar'] }}`. To avoid this, use `camelCase` or `snake_case` for names. +:: + +For lists, access elements by index with square brackets (`[]`): + +```twig +{{ foo[0] }} # Accesses the first element in the list 'foo' +``` + +--- + +### Filters + +Filters transform values and are applied using the pipe (`|`) symbol. Filters can be chained: + +```twig +{{ "Lemons to lemonade." | upper | abbreviate(10) }} +# Output: LEMONS TO ... +``` + +--- + +### Functions + +Functions generate new values. They are called with a name followed by parentheses: + +```twig +{{ max(user.score, highscore) }} +# Outputs the maximum of 'user.score' and 'highscore' +``` + +--- + +### Control Structures + +Pebble supports loops and conditionals to control the flow of templates. + +**For Loop:** + +```twig +{% for article in articles %} + {{ article.title }} +{% else %} + "No articles available." +{% endfor %} +``` + +**If Statement:** + +```twig +{% if category == "news" %} + {{ news }} +{% elseif category == "sports" %} + {{ sports }} +{% else %} + "Select a category" +{% endif %} +``` + +--- + +### Macros + +Macros are reusable template snippets, similar to functions: + +```twig +{% macro input(type, name) %} + {{ name }} is of type {{ type }} +{% endmacro %} +``` + +Usage: + +```twig +{{ input("text", "example") }} +# Output: example is of type text +``` + +Macros only access their local arguments. + +--- + +### Named Arguments + +Filters, functions, and macros support named arguments for clarity: + +```twig +{{ stringDate | date(existingFormat="yyyy-MMMM-d", format="yyyy/MMMM/d") }} +``` + +Named arguments can define defaults in macros: + +```twig +{% macro input(type="text", name, value="") %} + type: "{{ type }}", name: "{{ name }}", value: "{{ value }}" +{% endmacro %} + +{{ input(name="country") }} +# Output: type: "text", name: "country", value: "" +``` + +--- + +### Comments + +Add comments using `{# ... #}`. They do not appear in output: + +```twig +{# This is a comment #} +{{ "Visible content" }} +``` + +In YAML, use `#` for comments. + +--- + +### Literals + +Pebble supports literals for strings, numbers, booleans, and null values: +- `"Hello World"`: Strings use single or double quotes. +- `100 + 10l * 2.5`: Numbers include integers, longs, and floats. +- `true`, `false`: Boolean values. +- `null`: Represents no value. + +--- + +### Collections + +Create lists and maps directly: +- `["apple", "banana"]`: a list of strings. +- `{"apple":"red", "banana":"yellow"}`: a map of key-value pairs. + +--- + +### Math + +Basic mathematical operators are supported: +- `+`: Addition +- `-`: Subtraction +- `*`: Multiplication +- `/`: Division +- `%`: Modulus + +--- + +### Logical Operators + +Combine expressions using: +- `and`: True if both are true. +- `or`: True if either is true. +- `not`: Negates an expression. + +--- + +### Comparisons + +Pebble supports common comparison operators: +`==`, `!=`, `<`, `>`, `<=`, `>=`. + +--- + +### Tests + +Use the `is` operator to test expressions: + +```twig +{% if 3 is odd %} + "Odd number" +{% endif %} +``` + +Negate tests with `is not`: + +```twig +{% if name is not null %} + "Name exists" +{% endif %} +``` + +--- + +### Conditional (Ternary) Operator + +The conditional operator (`?`) works like Java's ternary operator: + +```twig +{{ foo ? "yes" : "no" }} +``` + +--- + +### Null-Coalescing Operator + +The `??` operator provides a fallback if a variable is null: + +```twig +{{ foo ?? bar ?? "default" }} +``` + +Raises an exception if no variable is defined: + +```twig +{{ foo ?? bar ?? raise }} +``` + +--- + +### Operator Precedence + +Operators are evaluated in the following order: +1. `.` +2. `|` +3. `%`, `/`, `*` +4. `-`, `+` +5. `==`, `!=`, `>`, `<`, `>=`, `<=` +6. `is`, `is not` +7. `and` +8. `or` + +--- + + +## Basic Filters + +Filters transform variables in expressions, allowing for operations like formatting, string manipulation, and list processing. Filters are applied using the pipe symbol (`|`) and can be chained together. + +To apply a filter, use this syntax: + +```twig +{{ name | title }} +``` +This example converts `name` to title case. + +Filters that accept arguments use parentheses. For example, to join a list of strings with commas: + +```twig +{{ list | join(', ') }} +``` + +To apply a filter to a block of text, wrap it with the `filter` tag: + +```twig +{% filter lower | title %} + hello world +{% endfilter %} +``` + +--- + +## JSON Filters + +JSON filters are specifically designed to manipulate JSON objects, such as API responses. + +### `toJson` + +The `toJson` filter converts any object into a JSON string. Examples: + +```twig +{{ [1, 2, 3] | toJson }} # Outputs: '[1, 2, 3]' +{{ true | toJson }} # Outputs: 'true' +{{ "foo" | toJson }} # Outputs: '"foo"' +``` + +::alert{type="info"} +In versions prior to [v0.18.0](/blogs/2024-08-06-release-0-18.md), this filter was named `json`. Using `json` will still work but raises a warning in the UI. :: + +--- + +### `jq` + +The `jq` filter applies a [JQ expression](https://stedolan.github.io/jq/) to a variable. The result is always an array formatted as JSON. Use the `first` filter to extract the first (or only) result. + +Examples: + +```twig +{{ [1, 2, 3] | jq('.') }} +# Outputs: '[1, 2, 3]' + +{{ [1, 2, 3] | jq('.[0]') | first }} +# Outputs: '1' +``` + +Given the context: + +```json +{ + "outputs": { + "task1": { + "value": 1, + "text": "awesome1" + }, + "task2": { + "value": 2, + "text": "awesome2" + } + } +} +``` + +The following expression extracts the `value` of `task1`: + +```twig +{{ outputs | jq('.task1.value') | first }} +# Outputs: '1' +``` + +**Arguments** +- `expression`: The JQ expression to apply. + +--- + +### Manipulating JSON Payloads + +Here is a comprehensive example of JSON manipulation. This flow takes a JSON payload as input and performs multiple transformations: + +```yaml +id: myflow +namespace: company.myteam + +inputs: + - id: payload + type: JSON + defaults: |- + { + "name": "John Doe", + "score": { + "English": 72, + "Maths": 88, + "French": 95, + "Spanish": 85, + "Science": 91 + }, + "address": { + "city": "Paris", + "country": "France" + }, + "graduation_years": [2020, 2021, 2022, 2023] + } + +tasks: + - id: print_status + type: io.kestra.plugin.core.log.Log + message: + - "Student name: {{ inputs.payload.name }}" # Extracting a value + - "Score in languages: {{ inputs.payload.score.English + inputs.payload.score.French + inputs.payload.score.Spanish }}" # Summing numbers + - "Total subjects: {{ inputs.payload.score | length }}" # Counting keys in a map + - "Total score: {{ inputs.payload.score | values | jq('reduce .[] as $num (0; .+$num)') | first }}" # Summing all values + - "Complete address: {{ inputs.payload.address.city }}, {{ inputs.payload.address.country | upper }}" # Concatenation and transformation + - "Total years for graduation: {{ inputs.payload.graduation_years | length }}" # Counting array elements + - "Started college in: {{ inputs.payload.graduation_years | first }}" # First element in an array + - "Completed college in: {{ inputs.payload.graduation_years | last }}" # Last element in an array +``` + +Running this flow will log: + +``` +Student name: John Doe +Score in languages: 252 +Total subjects: 5 +Total score: 431 +Complete address: Paris, FRANCE +Total years for graduation: 4 +Started college in: 2020 +Completed college in: 2023 +``` + +--- + +## Numeric Filters + +Numeric filters are used to format numbers or convert strings to numbers. + +### abs + +The `abs` filter returns the absolute value of a number. + +```twig +{{ -7 | abs }} +# output: 7 +``` + +### number + +The `number` filter parses a string into a number. If no type is specified, the type is inferred. + +```twig +{{ "12.3" | number | className }} +# output: java.lang.Float + +{{ "9223372036854775807" | number('BIGDECIMAL') | className }} +# output: java.math.BigDecimal +``` + +- type: + - `INT` + - `FLOAT` + - `LONG` + - `DOUBLE` + - `BIGDECIMAL` + - `BIGINTEGER` + +### numberFormat + +The `numberFormat` filter formats a number using `java.text.DecimalFormat`. + +```twig +{{ 3.141592653 | numberFormat("#.##") }} +# output: 3.14 +``` + + +## Object Filters + +Object filters manipulate collections such as maps, arrays, or strings. + +### chunk + +The `chunk` filter partitions a list into chunks of the specified size. + +```twig +{{ [1, 2, 3, 4, 5] | chunk(2) }} +# results in: [[1, 2], [3, 4], [5]] +``` + +### className + +The `className` filter returns the class name of an object. + +```twig +{{ "12.3" | number | className }} +# output: java.lang.Float +``` + +### first + +The `first` filter retrieves the first item of a collection or the first character of a string. + +```twig +{{ ['apple', 'banana'] | first }} +# output: apple + +{{ 'Mitch' | first }} +# output: M +``` + +### join + +The `join` filter concatenates the items in a collection into a single string, separated by a specified delimiter. + +```twig +{{ ['apple', 'banana'] | join(', ') }} +# output: apple, banana +``` + +### keys + +The `keys` filter retrieves the keys from a map or the indices of an array. + +```twig +{{ {'foo': 'bar', 'baz': 'qux'} | keys }} +# output: ['foo', 'baz'] +``` + +### values + +The `values` filter retrieves the values from a map. + +```twig +{{ {'foo': 'bar', 'baz': 'qux'} | values }} +# output: ['bar', 'qux'] +``` + +### last + +The `last` filter retrieves the last item of a collection or the last character of a string. + +```twig +{{ ['apple', 'banana'] | last }} +# output: banana + +{{ 'Mitch' | last }} +# output: h +``` + +### length + +The `length` filter returns the size of a collection or the length of a string. + +```twig +{{ 'Mitch' | length }} +# output: 5 +``` + +### merge + +The `merge` filter combines two collections (lists or maps). + +```twig +{{ [1, 2] | merge([3, 4]) }} +# output: [1, 2, 3, 4] +``` + +### reverse + +The `reverse` filter reverses the order of items in a collection. + +```twig +{{ ['apple', 'banana'] | reverse }} +# output: ['banana', 'apple'] +``` + +### rsort + +The `rsort` filter sorts a list in reverse order. + +```twig +{{ [3, 1, 2] | rsort }} +# output: [3, 2, 1] +``` + +### slice + +The `slice` filter extracts a portion of a collection or string. + +```twig +{{ ['apple', 'banana', 'cherry'] | slice(1, 2) }} +# output: ['banana'] + +{{ 'Mitch' | slice(1, 3) }} +# output: it +``` + +**Arguments**: +- `fromIndex`: starting index (inclusive). +- `toIndex`: ending index (exclusive). + +### sort + +The `sort` filter sorts a collection in ascending order. + +```twig +{{ [3, 1, 2] | sort }} +# output: [1, 2, 3] +``` + +### split + +The `split` filter divides a string into a list based on a delimiter. + +```twig +{{ 'apple,banana,cherry' | split(',') }} +# output: ['apple', 'banana', 'cherry'] +``` + +**Arguments**: +- `delimiter`: the string to split on. +- `limit`: limits the number of splits: + - **Positive**: limits the array size, with the last entry containing the remaining content. + - **Zero or negative**: no limit on splits. + +```twig +{{ 'apple,banana,cherry,grape' | split(',', 2) }} +# output: ['apple', 'banana,cherry,grape'] +``` + + +--- + +## String Filters + +String filters manipulate textual data, enabling operations like transformation, encoding, or formatting. + +### abbreviate + +The `abbreviate` filter shortens a string using an ellipsis. The `length` includes the ellipsis. + +```twig +{{ "this is a long sentence." | abbreviate(7) }} +# output: this... +``` + +**Arguments**: +- length: the maximum length of the output. + +--- + +### base64decode + +The `base64decode` filter decodes a base64-encoded string into UTF-8. + +```twig +{{ "dGVzdA==" | base64decode }} +# output: test +``` + +Throws an exception for invalid base64 strings. + +--- + +### base64encode + +The `base64encode` filter encodes a string to base64. + +```twig +{{ "test" | base64encode }} +# output: dGVzdA== +``` + +--- + +### capitalize + +The `capitalize` filter capitalizes the first letter of a string. + +```twig +{{ "article title" | capitalize }} +# output: Article title +``` + +--- + +### title + +The `title` filter capitalizes the first letter of each word. + +```twig +{{ "article title" | title }} +# output: Article Title +``` + +--- + +### default + +The `default` filter provides a fallback value for empty variables. + +```twig +{{ user.phoneNumber | default("No phone number") }} +# output: No phone number (if user.phoneNumber is empty) +``` + +Suppresses exceptions if the attribute is missing. + +--- + +### escapeChar + +The `escapeChar` filter escapes special characters in a string. + +```twig +{{ "Can't be here" | escapeChar('single') }} +# output: Can\'t be here +``` + +**Arguments**: +- `type`: escape type (`single`, `double`, or `shell`). + +--- + +### lower + +The `lower` filter converts a string to lowercase. + +```twig +{{ "LOUD TEXT" | lower }} +# output: loud text +``` + +--- + +### replace + +The `replace` filter replaces substrings in a string with specified values. + +```twig +{{ "I like %this% and %that%." | replace({'%this%': foo, '%that%': "bar"}) }} +# output: I like foo and bar +``` + +**Arguments**: +- `replace_pairs`: a map of search-replace pairs. +- `regexp`: enables regex-based replacements. + +--- + +### sha256 + +The `sha256` filter generates a SHA-256 hash of a string. + +```twig +{{ "test" | sha256 }} +# output: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 +``` + +--- + +### startsWith + +The `startsWith` filter checks if a string starts with a given prefix. + +```twig +{{ "hello world" | startsWith("hello") }} +# output: true +``` + +--- + +### slugify + +The `slugify` filter converts a string to a URL-friendly format. + +```twig +{{ "Hello World!" | slugify }} +# output: hello-world +``` + +--- + +### substringAfter + +The `substringAfter` filter extracts the substring after the first occurrence of a `separator`. + +```twig +{{ "a.b.c" | substringAfter(".") }} +# output: b.c +``` + +--- + +### substringAfterLast + +The `substringAfterLast` filter extracts the substring after the last occurrence of a `separator`. + +```twig +{{ "a.b.c" | substringAfterLast(".") }} +# output: c +``` + +--- + +### substringBefore + +The `substringBefore` filter extracts the substring before the first occurrence of a `separator`. + +```twig +{{ "a.b.c" | substringBefore(".") }} +# output: a +``` + +--- + +### substringBeforeLast + +The `substringBeforeLast` filter extracts the substring before the last occurrence of a `separator`. + +```twig +{{ "a.b.c" | substringBeforeLast(".") }} +# output: a.b +``` + +--- + +### trim + +The `trim` filter removes whitespace from the start and end of a string. + +```twig +{{ " padded text " | trim }} +# output: padded text +``` + +--- + +### upper + +The `upper` filter converts a string to uppercase. + +```twig +{{ "quiet sentence" | upper }} +# output: QUIET SENTENCE +``` + +--- + +### urldecode + +The `urldecode` filter decodes a URL-encoded string. + +```twig +{{ "The+string+%C3%BC%40foo-bar" | urldecode }} +# output: The string ü@foo-bar +``` + +--- + +### urlencode + +The `urlencode` filter encodes a string for URLs. + +```twig +{{ "The string ü@foo-bar" | urlencode }} +# output: The+string+%C3%BC%40foo-bar +``` + +--- + +## Temporal Filters + +Temporal filters are used for formatting, manipulating, and converting dates and timestamps. + +### date + +The `date` filter formats a date object or string into a specified format. It supports `java.util.Date`, `java.time` constructs like `OffsetDateTime`, and epoch timestamps in milliseconds. + +```twig +{{ user.birthday | date("yyyy-MM-dd") }} +# output: 2001-07-24 +``` + +To format a string-based date, provide the desired output format and the existing format of the string: + +```twig +{{ "July 24, 2001" | date("yyyy-MM-dd", existingFormat="MMMM dd, yyyy") }} +# output: 2001-07-24 +``` + +#### Time Zones + +Specify a custom time zone using the `timeZone` argument: + +```twig +{{ now() | date("yyyy-MM-dd'T'HH:mm:ssX", timeZone="UTC") }} +``` + +**Arguments**: +- `format`: the desired output format. +- `existingFormat`: the input format (if parsing a string). +- `timeZone`: the time zone for formatting. +- `locale`: the locale for formatting. + +#### Supported Date Formats + +- Standard Java formats: [DateTimeFormatter](https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html) +- Presets: `iso`, `sql`, `iso_date_time`, `iso_zoned_date_time`, etc. + +--- + +### dateAdd + +The `dateAdd` filter adds or subtracts a specified amount of time to/from a date. + +```twig +{{ now() | dateAdd(-1, 'DAYS') }} +# output: 2024-07-08T06:17:01.174686Z +``` + +**Arguments**: +- `amount`: an integer specifying the time to add/subtract. +- `unit`: the time unit (e.g., `DAYS`, `HOURS`, `YEARS`). +- Additional arguments: same as the `date` filter. + +--- + +### timestamp + +The `timestamp` filter converts a date to a Unix timestamp in seconds. + +```twig +{{ now() | timestamp(timeZone="Europe/Paris") }} +# output: 1720505821 +``` + +**Arguments**: +- `existingFormat`: the input format (if parsing a string). +- `timeZone`: the time zone for conversion. + +--- + +### timestampMicro + +The `timestampMicro` filter converts a date to a Unix timestamp in microseconds. + +```twig +{{ now() | timestampMicro(timeZone="Asia/Kolkata") }} +# output: 1720505821000180275 +``` + +**Arguments**: +- Same as `timestamp`. + +--- + +### timestampNano + +The `timestampNano` filter converts a date to a Unix timestamp in nanoseconds. + +```twig +{{ now() | timestampNano(timeZone="Asia/Kolkata") }} +# output: 1720505821182413000 +``` + +**Arguments**: +- Same as `timestamp`. + +--- + +### Example with Temporal Filters + +Here’s an example flow showcasing the use of temporal filters: + +```yaml +id: temporal-dates +namespace: company.myteam + +tasks: + - id: print_status + type: io.kestra.plugin.core.log.Log + message: + - "Present timestamp: {{ now() }}" + - "Formatted timestamp: {{ now() | date('yyyy-MM-dd') }}" + - "Previous day: {{ now() | dateAdd(-1, 'DAYS') }}" + - "Next day: {{ now() | dateAdd(1, 'DAYS') }}" + - "Timezone (seconds): {{ now() | timestamp(timeZone='Asia/Kolkata') }}" + - "Timezone (microseconds): {{ now() | timestampMicro(timeZone='Asia/Kolkata') }}" + - "Timezone (nanoseconds): {{ now() | timestampNano(timeZone='Asia/Kolkata') }}" +``` + +Running this flow will log the following: + +``` +Present timestamp: 2024-07-09T06:17:01.171193Z +Formatted timestamp: 2024-07-09 +Previous day: 2024-07-08T06:17:01.174686Z +Next day: 2024-07-10T06:17:01.176138Z +Timezone (seconds): 1720505821 +Timezone (microseconds): 1720505821000180275 +Timezone (nanoseconds): 1720505821182413000 +``` + +--- + +## YAML Filters + +YAML filters allow you to parse and manipulate YAML strings, converting them into objects that can be processed further. + +--- + +### yaml + +The `yaml` filter, introduced in [Kestra 0.16.0](https://github.com/kestra-io/kestra/pull/3283), parses a YAML string into an object. This is especially useful when working with templated tasks, such as the [TemplatedTask](/plugins/tasks/templating/io.kestra.plugin.core.templating.TemplatedTask). + +Example: + +```twig +{{ "foo: bar" | yaml }} +``` + +#### Example: Using the `yaml` filter in a templated task + +```yaml +id: yaml_filter_example +namespace: company.team + +tasks: + - id: yaml_filter + type: io.kestra.plugin.core.log.Log + message: | + {{ "foo: bar" | yaml }} + {{ {"key": "value"} | yaml }} +``` + +--- + +### indent + +The `indent` filter adds indentation to strings, applying the specified number of spaces before each line (except the first). + +**Arguments**: +- `amount`: number of spaces to add. +- `prefix`: the string used for indentation (default is `" "`). + +Example: + +```twig +{{ "key: value" | indent(2) }} +# output: + key: value +``` + +--- + +### nindent + +The `nindent` filter adds a newline before the input and then indents all lines. + +**Arguments**: +- `amount`: number of spaces for indentation. +- `prefix`: the string used for indentation (default is `" "`). + +Example: + +```twig +{{ "key: value" | nindent(2) }} +# output: + key: value +``` + +--- + +### Example with `indent` and `nindent` + +```yaml +id: templated_task_example +namespace: company.team + +labels: + example: test + +variables: + yaml_data: | + key1: value1 + key2: value2 + +tasks: + - id: yaml_with_indent + type: io.kestra.plugin.core.templating.TemplatedTask + spec: | + id: example-task + type: io.kestra.plugin.core.log.Log + message: | + Metadata: + {{ labels | yaml | indent(4) }} + + Variables: + {{ variables.yaml_data | yaml | nindent(4) }} +``` + +The above example generates a task with indented YAML content for both `labels` and `variables`. + +Here is an explanation of the filters used: +- Using `yaml`: converts the YAML string into an object. +- Using `indent(4)`: adds four spaces before each line. +- Using `nindent(4)`: adds a newline and then indents with four spaces. + +--- + +## Functions + +Functions in Kestra allow you to dynamically generate or manipulate content. They are invoked by their name followed by parentheses `()` and can accept arguments. + +--- + +### block + +The `block` function renders the contents of a block multiple times. It is distinct from the `block` tag used to declare blocks. + +Example: + +```twig +{% block "post" %}content{% endblock %} + +{{ block("post") }} +``` + +Output: +``` +content +content +``` + +--- + +### currentEachOutput + +The `currentEachOutput` function simplifies retrieving outputs of sibling tasks within an `EachSequential` task. + +Example: + +```yaml +tasks: + - id: each + type: io.kestra.plugin.core.flow.EachSequential + tasks: + - id: first + type: io.kestra.plugin.core.debug.Return + format: "{{task.id}}" + - id: second + type: io.kestra.plugin.core.debug.Return + format: "{{ currentEachOutput(outputs.first).value }}" + value: ["value 1", "value 2", "value 3"] +``` + +This eliminates the need for manual handling of `taskrun.value` or `parents`. + +--- + +### fromJson + +The `fromJson` function parses a JSON string into an object, enabling property access. + +Examples: + +```twig +{{ fromJson('[1, 2, 3]')[0] }} +# output: 1 + +{{ fromJson('{"foo": [666, 1, 2]}').foo[0] }} +# output: 666 +``` + +--- + +### yaml + +The `yaml` function parses a YAML string into an object. + +Example: + +```twig +{{ yaml('foo: [666, 1, 2]').foo[0] }} +# output: 666 +``` + +--- + +### max + +The `max` function returns the largest of its arguments. + +Example: + +```twig +{{ max(20, 80, user.age) }} +# output: the largest value +``` + +--- + +### min + +The `min` function returns the smallest of its arguments. + +Example: + +```twig +{{ min(20, 80, user.age) }} +# output: the smallest value +``` + +--- + +### now + +The `now` function generates the current datetime. Formatting options are the same as the `date` filter. + +Example: + +```twig +{{ now() }} +{{ now(timeZone="Europe/Paris") }} +``` + +--- + +### parent + +The `parent` function renders the parent block's content within a child block. + +Example: + +Parent template (`parent.peb`): + +```twig +{% block "content" %}parent content{% endblock %} +``` + +Child template (`child.peb`): + +```twig +{% extends "parent.peb" %} + +{% block "content" %} +child content +{{ parent() }} +{% endblock %} +``` + +Output: + +``` +child content +parent content +``` + +--- + +### range + +The `range` function generates a list of numbers. + +Examples: + +```twig +{% for i in range(0, 3) %} + {{ i }}, +{% endfor %} +# output: 0, 1, 2, 3 + +{% for i in range(0, 6, 2) %} + {{ i }}, +{% endfor %} +# output: 0, 2, 4, 6 +``` + +--- + +### printContext + +The `printContext` function is used for debugging by printing all defined variables. + +Example: + +```twig +{{ printContext() }} +``` + +Output: + +```json +{"outputs": {...}, "execution": {...}, ...} +``` + +--- + +### read + +The `read` function retrieves the contents of a file from internal storage or namespace files. + +Examples: + +```twig +{{ read('subdir/file.txt') }} +{{ read(outputs.someTask.uri) }} +``` + +--- + +### render + +The `render` function enables recursive rendering of expressions. By default, Kestra only renders expressions once. + +Example: + +```twig +{{ render("{{ trigger.date ?? execution.startDate | date('yyyy-MM-dd') }}") }} +``` + +**Arguments**: +- `recursive`: defaults to `true`. Set to `false` for one-time rendering. + +--- + +### renderOnce + +Equivalent to `render(expression, recursive=false)`. It simplifies rendering without recursion. + +--- + +### secret + +The `secret` function retrieves secrets stored in Kestra's secret backend. + +Example: + +```yaml +tasks: + - id: github_secret + type: io.kestra.plugin.core.log.Log + message: "{{ secret('GITHUB_ACCESS_TOKEN') }}" +``` + +--- + +### Example with Functions + +```yaml +id: function_example +namespace: company.team + +tasks: + - id: max_example + type: io.kestra.plugin.core.log.Log + message: "Maximum value: {{ max(5, 10, 15) }}" + + - id: render_example + type: io.kestra.plugin.core.log.Log + message: "{{ render('{{ trigger.date ?? execution.startDate | date("yyyy-MM-dd") }}') }}" + + - id: secret_example + type: io.kestra.plugin.core.log.Log + message: "{{ secret('API_KEY') }}" + allowFailure: true +``` + +--- + +## Operators + +Operators enable logical, arithmetic, and comparison operations within templated expressions. They are essential for dynamic content manipulation. + +### Comparison Operators + +Supported comparison operators: `==`, `!=`, `<`, `>`, `<=`, `>=`. + +- `==`: Uses `Java.util.Objects.equals(a, b)` for null-safe comparisons. Alias: `equals`. + +Example: + +```twig +{% if user.name equals "Mitchell" %} + ... +{% endif %} +``` + +--- + +### concat + +The `~` operator concatenates two or more strings. + +Example: + +```twig +{{ "apple" ~ "pear" ~ "banana" }} +# results in: 'applepearbanana' +``` + +--- + +### contains + +The `contains` operator checks if an item exists within a collection, map, or array. + +Examples: + +```twig +{% if ["apple", "pear", "banana"] contains "apple" %} + ... +{% endif %} +``` + +For maps, it checks for an existing key: + +```twig +{% if {"apple":"red", "banana":"yellow"} contains "banana" %} + ... +{% endif %} +``` + +To check multiple items: + +```twig +{% if ["apple", "pear", "banana", "peach"] contains ["apple", "peach"] %} + ... +{% endif %} +``` + +--- + +### is + +The `is` operator tests variables, returning a boolean. + +Examples: + +```twig +{% if 2 is even %} + ... +{% endif %} +``` + +Negation with `not`: + +```twig +{% if 3 is not even %} + ... +{% endif %} +``` + +--- + +### logic + +Combine boolean expressions using `and` and `or`. Use `not` for negation. + +Examples: + +```twig +{% if 2 is even and 3 is odd %} + ... +{% endif %} + +{% if 3 is not even %} + ... +{% endif %} +``` + +Group expressions with parentheses for precedence: + +```twig +{% if (3 is not even) and (2 is odd or 3 is even) %} + ... +{% endif %} +``` + +--- + +### math + +Perform arithmetic operations with standard math operators. Follow the order of operations. + +Example: + +```twig +{{ 2 + 2 / (10 % 3) * (8 - 1) }} +``` + +Supported operators: +- `+`: Addition +- `-`: Subtraction +- `/`: Division (returns a float) +- `%`: Modulus +- `*`: Multiplication + +--- + +### not + +Use `not` with `is` to negate a test. + +Example: + +```twig +{% if 3 is not even %} + ... +{% endif %} +``` + +--- + +### null-coalescing + +The null-coalescing operator (`??`) returns the first defined, non-null value. Use `???` to return the right-hand side only if the left-hand side is undefined. + +Examples: + +```twig +{% set baz = "baz" %} +{{ foo ?? bar ?? baz }} +# results in: 'baz' + +{{ foo ?? bar ?? raise }} +# raises an exception if all variables are undefined +``` + +For details, see the [Handling null and undefined values](../15.how-to-guides/null-values.md) guide. + +--- + +### ternary operator + +The ternary operator (`? :`) evaluates conditions succinctly. + +Example: + +```twig +{{ foo == null ? bar : baz }} +``` + + +--- + +## Tag + +Tags in Pebble control the template's flow and logic. They are enclosed in `{% %}`. + +--- + +### block + +The `block` tag defines reusable template blocks. + +Example: + +```twig +{% block header %} + Introduction +{% endblock %} +``` + +To reuse a block, use the block function: + +```twig +{{ block("header") }} +``` + +--- + +### filter + +The `filter` tag applies a filter to a block of content. + +Example: + +```twig +{% filter upper %} + hello +{% endfilter %} +``` + +Output: + +``` +HELLO +``` + +Filters can be chained: + +```twig +{% filter upper | title %} + hello +{% endfilter %} +``` + +Output: + +``` +Hello +``` + +--- + +### for + +The `for` tag iterates over arrays, maps, or any `java.lang.Iterable`. + +Example: + +```twig +{% for user in users %} + {{ user.name }} lives in {{ user.city }}. +{% endfor %} +``` + +Special variables available within a loop: +- `loop.index`: zero-based index +- `loop.length`: total size of the iterable +- `loop.first`: true if it's the first iteration +- `loop.last`: true if it's the last iteration +- `loop.revindex`: iterations remaining until the end + +```twig +{% for user in users %} + {{ loop.index }}: {{ user.id }} +{% endfor %} +``` + +To handle empty collections, use the `else` tag: + +```twig +{% for user in users %} + {{ user.name }} +{% else %} + No users found. +{% endfor %} +``` + +For maps: + +```twig +{% for entry in map %} + {{ entry.key }}: {{ entry.value }} +{% endfor %} +``` + +--- + +### if + +The `if` tag evaluates conditional logic. + +Example: + +```twig +{% if users is empty %} + No users available. +{% elseif users.length == 1 %} + One user found. +{% else %} + Multiple users found. +{% endif %} +``` + +`if` expressions can include: +- `boolean` values +- `is` operator (e.g., `is empty`, `is not empty`) + +--- + +### macro + +The `macro` tag defines reusable blocks of content. + +Example: + +```twig +{% macro input(type="text", name, value) %} + +{% endmacro %} + +{{ input(name="username") }} +``` + +Output: + +```html + +``` + +**Passing global context**: + +```twig +{% set foo = 'bar' %} + +{{ test(_context) }} +{% macro test(_context) %} + {{ _context.foo }} +{% endmacro %} +``` + +Output: + +``` +bar +``` + +--- + +### raw + +The `raw` tag prevents Pebble from parsing its content. + +Example: + +```twig +{% raw %} + {{ user.name }} +{% endraw %} +``` + +Output: + +``` +{{ user.name }} +``` + +--- + +### set + +The `set` tag defines a variable in the template context. + +Example: + +```twig +{% set header = "Welcome Page" %} + +{{ header }} +``` + +Output: + +``` +Welcome Page +``` + +--- + +## Test + +Tests in Pebble are used to perform logical checks, such as determining if a variable is defined, empty, or of a specific type. + +--- + +### defined + +Checks if a variable is defined. + +```twig +{% if missing is not defined %} + ... +{% endif %} +``` + +--- + +### empty + +Checks if a variable is empty. A variable is considered empty if it is: +- null +- an empty string +- an empty collection +- an empty map + +```twig +{% if user.email is empty %} + ... +{% endif %} +``` + +--- + +### even + +Checks if an integer is even. + +```twig +{% if 2 is even %} + ... +{% endif %} +``` + +--- + +### iterable + +Checks if a variable implements `java.lang.Iterable`. + +```twig +{% if users is iterable %} + {% for user in users %} + ... + {% endfor %} +{% endif %} +``` + +--- + +### json + +Checks if a variable is a valid JSON string. + +```twig +{% if '{"test": 1}' is json %} + ... +{% endif %} +``` + +--- + +### map + +Checks if a variable is an instance of a map. + +```twig +{% if {"apple":"red", "banana":"yellow"} is map %} + ... +{% endif %} +``` + +--- + +### null + +Checks if a variable is null. + +```twig +{% if user.email is null %} + ... +{% endif %} +``` + +--- + +### odd + +Checks if an integer is odd. + +```twig +{% if 3 is odd %} + ... +{% endif %} +``` + diff --git a/nuxt.config.ts b/nuxt.config.ts index eb3697c4cc..3ff44cbe17 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -178,7 +178,6 @@ export default defineNuxtConfig({ "Build with Kestra": [ "Concepts", "Workflow Components", - "Expressions", "Version Control & CI/CD", "Plugin Developer Guide", "How-to Guides" @@ -195,6 +194,7 @@ export default defineNuxtConfig({ ], "Reference Docs": [ "Configuration", + "Expressions", "API Reference", "Terraform Provider", "Server CLI" @@ -233,8 +233,8 @@ export default defineNuxtConfig({ '/docs/workflow-components/triggers/conditions': {redirect: '/docs/workflow-components/triggers#conditions'}, '/docs/workflow-components/flow-properties': {redirect: '/docs/workflow-components/flow'}, '/docs/workflow-components/task-defaults': {redirect: '/docs/workflow-components/plugin-defaults'}, - '/docs/concepts/expression/02a.expression-types': {redirect: '/docs/expressions/expression-types'}, - '/docs/concepts/expression/02b.expression-usage': {redirect: '/docs/expressions/expression-usage'}, + '/docs/concepts/expression/02a.expression-types': {redirect: '/docs/expressions'}, + '/docs/concepts/expression/02b.expression-usage': {redirect: '/docs/expressions'}, '/docs/concepts/expression': {redirect: '/docs/expressions'}, '/docs/expression': {redirect: '/docs/expressions'}, '/docs/migration-guide/core-script-tasks': {redirect: '/docs/migration-guide/0.11.0/core-script-tasks'}, diff --git a/public/docs/expressions/printContext.png b/public/docs/expressions/printContext.png new file mode 100644 index 0000000000000000000000000000000000000000..44d5c84d7781ae19c5e2b9dafa922a78a432e175 GIT binary patch literal 117951 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 literal 0 HcmV?d00001