From 521a2b274d7440ec293c9dfa12623e7a903a38f8 Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Mon, 9 Mar 2026 11:56:58 +0100 Subject: [PATCH 01/52] docs(release): create 1.4.0 release branch --- .../docs/11.migration-guide/v.1.4.0/index.mdx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/contents/docs/11.migration-guide/v.1.4.0/index.mdx diff --git a/src/contents/docs/11.migration-guide/v.1.4.0/index.mdx b/src/contents/docs/11.migration-guide/v.1.4.0/index.mdx new file mode 100644 index 00000000000..c2f238a1514 --- /dev/null +++ b/src/contents/docs/11.migration-guide/v.1.4.0/index.mdx @@ -0,0 +1,14 @@ +--- +title: 1.4.0 +icon: /src/contents/docs/icons/migration-guide.svg +release: 1.4.0 +description: Migration guides and deprecated features for Kestra version 1.4.0. +--- + +import ChildCard from "~/components/docs/ChildCard.astro" + +## 1.4.0 + +Deprecated features and migration guides for 1.4.0 and onwards. + + \ No newline at end of file From addb3cc2b881c12e877e41eacb769c68216a0a9c Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Wed, 18 Mar 2026 10:34:16 -0500 Subject: [PATCH 02/52] docs(prototype): split config and expression pages (#4241) * docs(prototype): split config and expression pages * docs(config-expr): pull full reference out and add text support * docs(configuration-expressions): remove full references and fix links * docs(configuration-expression): reframe away from full-reference and capture missing diffs --- .../01.tasks/00.flowable-tasks/index.md | 1 + .../01.tasks/02.taskruns/index.md | 2 ++ .../06.outputs/index.md | 26 ++++++++++++++++++ .../04.function-reference/index.md | 27 ++++++++++++------- 4 files changed, 46 insertions(+), 10 deletions(-) diff --git a/src/contents/docs/05.workflow-components/01.tasks/00.flowable-tasks/index.md b/src/contents/docs/05.workflow-components/01.tasks/00.flowable-tasks/index.md index c1da7888746..5d301552d0e 100644 --- a/src/contents/docs/05.workflow-components/01.tasks/00.flowable-tasks/index.md +++ b/src/contents/docs/05.workflow-components/01.tasks/00.flowable-tasks/index.md @@ -170,6 +170,7 @@ In this execution, you can access: - The iteration value i.e., the index of a loop (the loop index starts at 0) using the syntax `{{ taskrun.iteration }}` - The output of a sibling task using the syntax `{{ outputs.sibling[taskrun.value].value }}` +- The output from a previous iteration using `iterationOutput()`, for example `{{ iterationOutput() }}` for the same task or `{{ iterationOutput('taskId', taskrun.iteration - 1) }}` for a sibling task in an earlier iteration --- diff --git a/src/contents/docs/05.workflow-components/01.tasks/02.taskruns/index.md b/src/contents/docs/05.workflow-components/01.tasks/02.taskruns/index.md index 948a172efc6..3c964d4a516 100644 --- a/src/contents/docs/05.workflow-components/01.tasks/02.taskruns/index.md +++ b/src/contents/docs/05.workflow-components/01.tasks/02.taskruns/index.md @@ -97,6 +97,8 @@ tasks: ``` This produces two separate log entries, one with `1` and the other with `2`. +In `ForEach`, `taskrun.iteration` is often paired with `iterationOutput()` when you need to read a previous iteration's result. For example, `{{ iterationOutput('my_task', taskrun.iteration - 1) }}` fetches the output of `my_task` from the previous loop index. + ### 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: diff --git a/src/contents/docs/05.workflow-components/06.outputs/index.md b/src/contents/docs/05.workflow-components/06.outputs/index.md index 9afc3ac20ef..4876bcc760c 100644 --- a/src/contents/docs/05.workflow-components/06.outputs/index.md +++ b/src/contents/docs/05.workflow-components/06.outputs/index.md @@ -359,6 +359,32 @@ tasks: You can also use the `currentEachOutput` function to access the current tree task. See [Function Reference](../../expressions/04.function-reference/index.md) for more details. +If you need the output from a previous iteration of the same task, or from a sibling task in a previous iteration, use `iterationOutput()` instead. + +For example, this flow builds a running total by reading the previous iteration's output: + +```yaml +id: foreach_prefix_sum +namespace: company.team + +tasks: + - id: foreach + type: io.kestra.plugin.core.flow.ForEach + values: ["100", "200", "300"] + tasks: + - id: prefix_sum + type: io.kestra.plugin.core.debug.Return + format: >- + {% set idx = taskrun.iteration %} + {% if idx == 0 %} + {{ taskrun.value | trim | number }} + {% else %} + {{ iterationOutput('prefix_sum', idx - 1) | trim | number + taskrun.value | trim | number }} + {% endif %} +``` + +`iterationOutput()` defaults to the current task and the previous iteration, so `{{ iterationOutput() }}` is equivalent to reading the current task's output from `taskrun.iteration - 1`. + :::alert{type="warning"} Accessing sibling task outputs is impossible on [Parallel](/plugins/core/tasks/flows/io.kestra.plugin.core.flow.Parallel) as it runs tasks in parallel. ::: diff --git a/src/contents/docs/expressions/04.function-reference/index.md b/src/contents/docs/expressions/04.function-reference/index.md index fa77f70dd09..37871e0ebf2 100644 --- a/src/contents/docs/expressions/04.function-reference/index.md +++ b/src/contents/docs/expressions/04.function-reference/index.md @@ -33,7 +33,6 @@ Examples: These functions bridge expressions to external or stored data. Use them when the value is not already present in the execution context and must be resolved at runtime. - `secret()` reads a secret from Kestra's secret backend -- `credential()` reads a short-lived token from a managed EE credential - `read()` reads the contents of a namespace file or internal-storage file - `fileURI()` resolves a namespace file URI @@ -41,7 +40,6 @@ Examples: ```twig {{ secret('GITHUB_ACCESS_TOKEN') }} -{{ credential('my_oauth') }} {{ read('subdir/file.txt') }} {{ fileURI('my_file.txt') }} ``` @@ -70,6 +68,7 @@ This group is more situational, but it becomes valuable in complex flows where y - `errorLogs()` for error summaries in alerts - `currentEachOutput()` for simpler access to sibling outputs inside `ForEach` +- `iterationOutput()` to read outputs from previous or explicit `ForEach` iterations - `tasksWithState()` to inspect tasks by state - `appLink()` in Enterprise Edition to generate Kestra App URLs @@ -133,24 +132,32 @@ Use `secret()` for sensitive values: {{ secret('API_KEY') }} ``` -### `credential()` +### `currentEachOutput()` -In Enterprise Edition, use `credential()` to inject a short-lived token from a managed credential: +Use it inside `ForEach` flows to avoid manual `taskrun.value` indexing: ```twig -{{ credential('my_oauth') }} +{{ currentEachOutput(outputs.make_data).values.data }} ``` -Use [Execution Context Variables](../01.execution-context/index.md) for the setup model and a fuller HTTP example. `credential()` returns the token only, while the credential definition itself is managed in the Kestra UI. - -### `currentEachOutput()` +### `iterationOutput()` -Use it inside `ForEach` flows to avoid manual `taskrun.value` indexing: +Use `iterationOutput()` inside `ForEach` when the current iteration depends on work completed in an earlier loop index: ```twig -{{ currentEachOutput(outputs.make_data).values.data }} +{{ iterationOutput() }} +{{ iterationOutput('prefix_sum', taskrun.iteration - 1) }} ``` +Defaulting behavior: + +- `iterationOutput()` uses the current task and the previous iteration +- `iterationOutput('taskId')` uses the provided task and the previous iteration +- `iterationOutput(null, 2)` uses the current task and iteration `2` +- `iterationOutput('taskId', null)` uses the provided task and the previous iteration + +Guard the first iteration explicitly, because there is no previous iteration to read when `taskrun.iteration == 0`. + ### `errorLogs()` Useful for error notifications: From 8a2979fbcc93992c70924276ef15ccb1d8d369d6 Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Wed, 18 Mar 2026 11:33:24 -0500 Subject: [PATCH 03/52] docs(function-reference): recover credential() --- .../docs/expressions/04.function-reference/index.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/contents/docs/expressions/04.function-reference/index.md b/src/contents/docs/expressions/04.function-reference/index.md index 37871e0ebf2..bd306fd1894 100644 --- a/src/contents/docs/expressions/04.function-reference/index.md +++ b/src/contents/docs/expressions/04.function-reference/index.md @@ -33,6 +33,7 @@ Examples: These functions bridge expressions to external or stored data. Use them when the value is not already present in the execution context and must be resolved at runtime. - `secret()` reads a secret from Kestra's secret backend +- `credential()` reads a short-lived token from a managed EE credential - `read()` reads the contents of a namespace file or internal-storage file - `fileURI()` resolves a namespace file URI @@ -40,6 +41,7 @@ Examples: ```twig {{ secret('GITHUB_ACCESS_TOKEN') }} +{{ credential('my_oauth') }} {{ read('subdir/file.txt') }} {{ fileURI('my_file.txt') }} ``` @@ -132,6 +134,16 @@ Use `secret()` for sensitive values: {{ secret('API_KEY') }} ``` +### `credential()` + +In Enterprise Edition, use `credential()` to inject a short-lived token from a managed credential: + +```twig +{{ credential('my_oauth') }} +``` + +Use [Execution Context Variables](../01.execution-context/index.md) for the setup model and a fuller HTTP example. `credential()` returns the token only, while the credential definition itself is managed in the Kestra UI. + ### `currentEachOutput()` Use it inside `ForEach` flows to avoid manual `taskrun.value` indexing: From 84a938c3aedca5b8377368c048b599b23238388d Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Wed, 18 Mar 2026 11:56:16 -0500 Subject: [PATCH 04/52] docs(plugin-defaults): add cpu cores configuration and update defaults --- .../09.plugin-defaults/index.md | 34 +++++++++++++++++-- .../07.namespace-management/index.md | 18 ++++++++-- .../02.governance/worker-isolation/index.md | 4 ++- .../02.runtime-and-storage/index.md | 13 +++++++ .../04.plugins-and-execution/index.md | 11 ++++++ 5 files changed, 73 insertions(+), 7 deletions(-) diff --git a/src/contents/docs/05.workflow-components/09.plugin-defaults/index.md b/src/contents/docs/05.workflow-components/09.plugin-defaults/index.md index 8ec16b81fd3..134c7f0a4ca 100644 --- a/src/contents/docs/05.workflow-components/09.plugin-defaults/index.md +++ b/src/contents/docs/05.workflow-components/09.plugin-defaults/index.md @@ -97,6 +97,12 @@ kestra: region: "us-east-1" ``` +Global plugin defaults must be configured under `kestra.plugins.defaults`. + +:::alert{type="info"} +The legacy `kestra.tasks.defaults` property is still supported for backward compatibility, but it is deprecated. Use `kestra.plugins.defaults` for all new configurations. +::: + If you want to set defaults only for a specific task, you can do that too: ```yaml @@ -110,17 +116,35 @@ kestra: region: "us-east-1" ``` +### Precedence of global, flow, and task values + +Kestra applies plugin defaults in this order: + +1. Global plugin defaults from `kestra.plugins.defaults` +2. Flow-level `pluginDefaults` +3. Properties defined directly on the task + +That means flow-level defaults override global defaults, and task properties override non-forced defaults. + +If `forced: true` is set on a plugin default, that default overrides properties defined directly on the task. This is especially useful for governance and isolation use cases such as enforcing a task runner. + ## Plugin Defaults Enterprise Edition :::alert{type="info"} In the [Enterprise Edition](../../07.enterprise/index.mdx) or [Kestra Cloud](/cloud), plugin defaults can be configured directly in the UI under the **Plugin Defaults** tab of a Namespace. ::: -You can create them via form or directly as YAML code for the Namespace: +You can create them from the Namespace UI using the guided form or YAML editor: ![Plugin Default Form Creation](./plugin-default-creation.png) -Or click on **YAML** and, for example, paste the following: +The add/edit dialog lets you: + +- choose a predefined plugin type or enter a custom plugin type +- switch between a form view and a YAML view +- preview the generated YAML for an existing plugin default + +If you switch to **YAML**, you can paste content such as: ```yaml - type: io.kestra.plugin.aws.s3.Upload @@ -132,8 +156,12 @@ Or click on **YAML** and, for example, paste the following: ### Inherited Plugin Defaults -Plugin Defaults are inherited from the parent Namespace to children Namespaces. In the example above, the image shows the Plugin Default was created in the `kestra.company` Namespace. Navigating to the **Plugin Defaults** tab of a child Namespace, for example `kestra.company.data`, shows the parent Namespace's Plugin Defaults. This avoids having to recreate Plugin Defaults across children Namespaces, but it still allows for the children Namespaces to maintain their own isolated defaults if needed. +Plugin Defaults are inherited from the parent Namespace to children Namespaces. In the example above, the image shows the Plugin Default was created in the `kestra.company` Namespace. Navigating to the **Plugin Defaults** tab of a child Namespace, for example `kestra.company.data`, shows the parent Namespace's Plugin Defaults together with the Namespace they come from. This avoids having to recreate Plugin Defaults across children Namespaces, but it still allows for the children Namespaces to maintain their own isolated defaults if needed. ![Plugin Default Inheritance](./inherited-plugin-defaults.png) +### Import and export + +From the Namespace **Plugin Defaults** tab, you can also export the current Namespace plugin defaults to YAML and import them back into another Namespace. This is useful when promoting a curated set of defaults across environments or teams. +
diff --git a/src/contents/docs/07.enterprise/02.governance/07.namespace-management/index.md b/src/contents/docs/07.enterprise/02.governance/07.namespace-management/index.md index ebffcec6d11..608656c9a67 100644 --- a/src/contents/docs/07.enterprise/02.governance/07.namespace-management/index.md +++ b/src/contents/docs/07.enterprise/02.governance/07.namespace-management/index.md @@ -69,10 +69,20 @@ When building new flows in a Namespace, Namespace secrets are accessible from th Plugin Defaults can also be defined at the Namespace level. These plugin defaults are then applied for all tasks of the corresponding type defined in the flows under the same Namespace. -On the Namespaces page, select the Namespace where you want to define the plugin defaults and navigate to the **Plugin defaults** tab. You can add the plugin defaults here and save the changes by clicking on the **Save** button at the bottom of the page. +On the Namespaces page, select the Namespace where you want to define the plugin defaults and navigate to the **Plugin Defaults** tab. ![Define Plugin Defaults](./plugindefaults-namespaces.png) +From there, you can: + +- add a plugin default with a guided form +- switch between predefined plugin types and a custom plugin type +- switch between form mode and YAML mode +- preview the YAML for an existing plugin default +- export plugin defaults from the current Namespace +- import plugin defaults from a YAML file +- inspect inherited plugin defaults together with the parent Namespace they come from + You can reference secrets and variables defined with the same Namespace in the plugin defaults. In the example below, you no longer need to add the `password` property for the MySQL query task as it's defined in your Namespace-level `pluginDefaults`: @@ -90,6 +100,8 @@ tasks: fetchOne: true ``` +Namespace-level plugin defaults are inherited by child Namespaces. This makes it possible to define shared defaults once in a parent Namespace and let child Namespaces reuse them while still adding their own overrides when needed. + ### Default service account for SDK plugins Namespaces can now provide **default authentication credentials** that [SDK-based plugins](/plugins/plugin-kestra) use to run tasks such as [List all Namespaces](/plugins/plugin-kestra/kestra-namespaces/io.kestra.plugin.kestra.namespaces.list). This allows tasks relying on the [Kestra SDK](../../../api-reference/kestra-sdk/index.mdx) to call the API without hard-coding credentials inside the flow. @@ -193,7 +205,7 @@ github: token: "{{ secret('GITHUB_TOKEN') }}" ``` -Then, create another file for `task_defaults_marketing.yml`: +Then, create another file for `plugin_defaults_marketing.yml`: ```yaml - type: io.kestra.plugin.aws @@ -214,7 +226,7 @@ resource "kestra_namespace" "marketing" { namespace_id = "marketing" description = "Namespace for the marketing team" variables = file("variables_marketing.yml") - task_defaults = file("task_defaults_marketing.yml") + plugin_defaults = file("plugin_defaults_marketing.yml") } ``` diff --git a/src/contents/docs/07.enterprise/02.governance/worker-isolation/index.md b/src/contents/docs/07.enterprise/02.governance/worker-isolation/index.md index 6d0ebfd0f78..ff495b4fd94 100644 --- a/src/contents/docs/07.enterprise/02.governance/worker-isolation/index.md +++ b/src/contents/docs/07.enterprise/02.governance/worker-isolation/index.md @@ -75,7 +75,7 @@ For [Bash tasks](/plugins/plugin-script-shell/io.kestra.plugin.scripts.shell.scr ```yaml kestra: - tasks: + plugins: defaults: - type: io.kestra.plugin.scripts.shell.Commands forced: true @@ -85,6 +85,8 @@ kestra: type: io.kestra.plugin.scripts.runner.docker.Docker ``` +`kestra.tasks.defaults` still works for backward compatibility, but `kestra.plugins.defaults` is the current and recommended property. + Forced plugin defaults: - Ensure a property is set globally for a task, and no task can override it. - Are critical for security and governance — for example, to enforce Shell tasks to run as Docker containers. diff --git a/src/contents/docs/configuration/02.runtime-and-storage/index.md b/src/contents/docs/configuration/02.runtime-and-storage/index.md index 0ced4d2b85c..84940b48805 100644 --- a/src/contents/docs/configuration/02.runtime-and-storage/index.md +++ b/src/contents/docs/configuration/02.runtime-and-storage/index.md @@ -23,6 +23,19 @@ Queues and repositories must stay compatible: - JDBC queue with H2, MySQL, or PostgreSQL repository - Kafka queue with Elasticsearch repository in Enterprise Edition +## Allocated CPU cores + +Kestra sizes several internal thread pools based on the number of CPU cores available to the process. By default, it uses the number of CPU cores reported by the runtime environment. + +If you want Kestra to size those pools using a different value, set `kestra.allocated-cpu-cores`: + +```yaml +kestra: + allocated-cpu-cores: 2 +``` + +This is useful when you want to limit how aggressively Kestra allocates worker, scheduler, and queue-related threads without changing container limits or host-level CPU settings. + ## Database and datasources Start here if you are choosing the persistence layer for a new Kestra instance or moving from a local setup to a durable environment. In most teams, this is the first configuration page they revisit after initial installation. diff --git a/src/contents/docs/configuration/04.plugins-and-execution/index.md b/src/contents/docs/configuration/04.plugins-and-execution/index.md index a00ae7d1f53..59a3b3788d7 100644 --- a/src/contents/docs/configuration/04.plugins-and-execution/index.md +++ b/src/contents/docs/configuration/04.plugins-and-execution/index.md @@ -92,6 +92,17 @@ kestra: Plugin defaults are evaluated by the Executor and propagated to other components, so every server should use the same `kestra.plugins.defaults`. ::: +`kestra.plugins.defaults` is the canonical global configuration key. The older `kestra.tasks.defaults` key is still recognized for compatibility, but it is deprecated and should be replaced. + +Precedence works as follows: + +- global plugin defaults provide the base values +- flow-level `pluginDefaults` override global defaults +- task properties override non-forced defaults +- `forced: true` prevents the task from overriding that property + +Use non-forced defaults for convenience and consistency, and use forced defaults when the platform must enforce a value such as a specific task runner. + Enable or preconfigure plugin features globally: ```yaml From 4af0868dd6122979eebb01927f1284805d774b3e Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Wed, 18 Mar 2026 12:06:00 -0500 Subject: [PATCH 05/52] docs(password-policy): update configuration Part of https://github.com/kestra-io/kestra-ee/issues/6128 --- .../docs/07.enterprise/03.auth/invitations/index.md | 2 ++ .../basic-auth-troubleshooting/index.md | 2 ++ .../configuration/05.security-and-secrets/index.md | 11 +++++++++-- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/contents/docs/07.enterprise/03.auth/invitations/index.md b/src/contents/docs/07.enterprise/03.auth/invitations/index.md index 69097cacb62..c8f0e76309c 100644 --- a/src/contents/docs/07.enterprise/03.auth/invitations/index.md +++ b/src/contents/docs/07.enterprise/03.auth/invitations/index.md @@ -40,6 +40,8 @@ You can check the box to **Create user directly (skip invitation)** if one is no When a user receives an invitation, they can click on the link in the email to accept it. The user will be redirected to the Kestra login page, where they set up their account (i.e., create a password), or log in using SSO if it's enabled. +If password-based login is enabled, the password they choose must satisfy the instance password policy configured under `kestra.security.basic-auth`. See [Security and Secrets configuration](../../../configuration/05.security-and-secrets/index.md) for the available password policy settings. + ## Invite expiration time Users have 7 days to accept the invitation. After this period, the invitation will expire and must be reissued. diff --git a/src/contents/docs/10.administrator-guide/basic-auth-troubleshooting/index.md b/src/contents/docs/10.administrator-guide/basic-auth-troubleshooting/index.md index 4cef9b026fd..9aa81ac4bc7 100644 --- a/src/contents/docs/10.administrator-guide/basic-auth-troubleshooting/index.md +++ b/src/contents/docs/10.administrator-guide/basic-auth-troubleshooting/index.md @@ -23,6 +23,8 @@ Since Basic Authentication is now required, the `enabled` flag is ignored and sh For production deployments, we recommend setting a valid email address and a strong password in the configuration file. +If you use the Setup page to create credentials, the password must satisfy the password policy configured under `kestra.security.basic-auth`. See [Security and Secrets configuration](../../configuration/05.security-and-secrets/index.md) for the available password policy settings. + If you're upgrading to version 0.24, there are three possible scenarios for existing users. ### Scenario 1: The `enabled` flag is set to `true` diff --git a/src/contents/docs/configuration/05.security-and-secrets/index.md b/src/contents/docs/configuration/05.security-and-secrets/index.md index 775c057b429..a3aed79d386 100644 --- a/src/contents/docs/configuration/05.security-and-secrets/index.md +++ b/src/contents/docs/configuration/05.security-and-secrets/index.md @@ -251,15 +251,22 @@ kestra: expire-after: P30D ``` -For username/password auth, enforce password complexity explicitly: +For username/password auth, configure password complexity explicitly: ```yaml kestra: security: basic-auth: - password-regexp: "" + password-min-length: 8 + password-require-special: true + password-min-digits: 1 + password-min-lower-case: 1 + password-min-upper-case: 1 + password-allowed-special-characters: "!@#$%^&*" ``` +These rules apply anywhere Kestra asks a user to set or reset a password, including the initial setup flow, invitation acceptance, and user management screens. + ### Delete configuration files after startup If the runtime reads secrets from configuration files, delete them after startup so tasks cannot read them later from disk: From 33ecfa3aed15733541bc701a1b928c1ad060fbf4 Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Thu, 19 Mar 2026 10:58:58 -0500 Subject: [PATCH 06/52] docs(script-commands-triggers): add to polling and language how-to guides --- .../07.triggers/04.polling-trigger/index.md | 2 + .../docs/15.how-to-guides/golang/index.md | 57 ++++++++++++++++++ .../docs/15.how-to-guides/javascript/index.md | 51 ++++++++++++++++ .../docs/15.how-to-guides/python/index.md | 58 ++++++++++++++++++- .../docs/15.how-to-guides/shell/index.md | 54 +++++++++++++++++ src/contents/docs/16.scripts/index.mdx | 4 +- 6 files changed, 224 insertions(+), 2 deletions(-) diff --git a/src/contents/docs/05.workflow-components/07.triggers/04.polling-trigger/index.md b/src/contents/docs/05.workflow-components/07.triggers/04.polling-trigger/index.md index 9b0718e1f3f..94d34cfaebd 100644 --- a/src/contents/docs/05.workflow-components/07.triggers/04.polling-trigger/index.md +++ b/src/contents/docs/05.workflow-components/07.triggers/04.polling-trigger/index.md @@ -13,6 +13,8 @@ Polling triggers repeatedly check an external system at a fixed interval. When n Kestra provides polling triggers for a wide variety of systems, including databases, message queues, cloud storage, and FTP servers. +Polling triggers are not limited to external connectors. Some plugins also provide script-based polling triggers, allowing you to run code on an interval and emit only when a condition matches. For example, the script plugins for Python, Shell, Ruby, Go, and Node provide `ScriptTrigger` and `CommandsTrigger` variants for polling with code or commands. + The polling frequency is controlled by the `interval` property. When triggered, the flow has access to the polling results through the `trigger` variable, making the retrieved data immediately available for downstream tasks. ## Example diff --git a/src/contents/docs/15.how-to-guides/golang/index.md b/src/contents/docs/15.how-to-guides/golang/index.md index 9e62dd11b02..568475b0a01 100644 --- a/src/contents/docs/15.how-to-guides/golang/index.md +++ b/src/contents/docs/15.how-to-guides/golang/index.md @@ -214,3 +214,60 @@ tasks: Once this has executed, both the metrics can be viewed under **Metrics**. ![metrics](./metrics.png) + +## Automate Go with triggers + +You can also use Go code as polling logic by using `ScriptTrigger` or `CommandsTrigger`. These trigger types run Go code on an interval and start a flow execution only when the `exitCondition` matches. + +Use `ScriptTrigger` for inline Go code: + +```yaml +id: go_script_trigger +namespace: company.team + +triggers: + - id: script_failure + type: io.kestra.plugin.scripts.go.ScriptTrigger + interval: PT10S + exitCondition: "exit 1" + edge: true + script: | + package main + + func main() { + panic("boom") + } + +tasks: + - id: log + type: io.kestra.plugin.core.log.Log + message: "Triggered with exitCode={{ trigger.exitCode }} (condition={{ trigger.condition }})" +``` + +Use `CommandsTrigger` when you want to run Go commands instead: + +```yaml +id: commands_trigger +namespace: company.team + +triggers: + - id: commands_failure + type: io.kestra.plugin.scripts.go.CommandsTrigger + interval: PT10S + exitCondition: "exit 1" + edge: true + containerImage: golang + commands: + - go run missing.go + +tasks: + - id: log + type: io.kestra.plugin.core.log.Log + message: "Triggered with exitCode={{ trigger.exitCode }} (condition={{ trigger.condition }})" +``` + +These trigger types support: + +- `interval` to control how often the script or commands run +- `exitCondition` to match an exit code such as `exit 1`, or a regex or substring matched against emitted vars and failure logs +- `edge` to emit only on a transition from not matching to matching diff --git a/src/contents/docs/15.how-to-guides/javascript/index.md b/src/contents/docs/15.how-to-guides/javascript/index.md index bfd61110cb6..e16c724e46c 100644 --- a/src/contents/docs/15.how-to-guides/javascript/index.md +++ b/src/contents/docs/15.how-to-guides/javascript/index.md @@ -240,6 +240,57 @@ Kestra.timer('duration', end - start); Once this has executed, `duration` will be viewable under **Metrics**. ![metrics](./metrics.png) +## Automate JavaScript with triggers + +You can also use JavaScript itself as polling logic by using `ScriptTrigger` or `CommandsTrigger`. These trigger types run Node.js code on an interval and start a flow execution only when the `exitCondition` matches. + +Use `ScriptTrigger` for inline Node.js code: + +```yaml +id: node_script_trigger +namespace: company.team + +triggers: + - id: script_failure + type: io.kestra.plugin.scripts.node.ScriptTrigger + interval: PT10S + exitCondition: "exit 1" + edge: true + script: | + throw new Error("boom"); + +tasks: + - id: log + type: io.kestra.plugin.core.log.Log + message: "Triggered with exitCode={{ trigger.exitCode }}" +``` + +Use `CommandsTrigger` when you want to run Node.js commands instead: + +```yaml +id: node_commands_trigger +namespace: company.team + +triggers: + - id: on_fail + type: io.kestra.plugin.scripts.node.CommandsTrigger + interval: PT5S + exitCondition: "exit 1" + commands: + - node -e "throw new Error('boom')" + +tasks: + - id: log + type: io.kestra.plugin.core.log.Log + message: "Triggered with exitCode={{ trigger.exitCode }}" +``` + +These trigger types support: + +- `interval` to control how often the script or commands run +- `exitCondition` to match an exit code such as `exit 1`, or a regex or substring matched against emitted vars and failure logs +- `edge` to emit only on a transition from not matching to matching + ## Execute GraalVM Task Kestra also supports GraalVM integration, allowing you to execute JavaScript code directly on the JVM, with the potential for performance improvements. There are currently two tasks: diff --git a/src/contents/docs/15.how-to-guides/python/index.md b/src/contents/docs/15.how-to-guides/python/index.md index 8b3c0bccddf..dfd3910bcbe 100644 --- a/src/contents/docs/15.how-to-guides/python/index.md +++ b/src/contents/docs/15.how-to-guides/python/index.md @@ -341,12 +341,13 @@ flow.execute('example', 'python_scripts', {'greeting': 'hello from Python'}) Read more about it on the [execution page](../../05.workflow-components/03.execution/index.md). -## Automate Python with Triggers +## Automate Python with triggers You can combine your Python code with a trigger to automatically execute your code. There's a few key ways you can automate it: - Run on a schedule - Run when a webhook is called - Run when a file is available in a data lake or storage bucket +- Run Python code on a polling interval and emit only when a condition matches
@@ -440,6 +441,61 @@ triggers: maxKeys: 1 ``` +### Run Python as a polling trigger + +If you want the polling logic itself to be written in Python, you can use `ScriptTrigger` or `CommandsTrigger`. These triggers run Python code on an interval and start a flow execution only when the `exitCondition` matches the result. + +Use `ScriptTrigger` for inline Python code: + +```yaml +id: python_script_trigger +namespace: company.team + +triggers: + - id: script_failure + type: io.kestra.plugin.scripts.python.ScriptTrigger + interval: PT10S + exitCondition: "exit 1" + edge: true + script: | + raise Exception("boom") + +tasks: + - id: log + type: io.kestra.plugin.core.log.Log + message: "Triggered with exitCode={{ trigger.exitCode }} (condition={{ trigger.condition }})" +``` + +Use `CommandsTrigger` when you want to run Python commands instead: + +```yaml +id: python_commands_trigger +namespace: company.team + +triggers: + - id: on_fail + type: io.kestra.plugin.scripts.python.CommandsTrigger + interval: PT10S + exitCondition: "exit 1" + edge: true + containerImage: python:3.13-slim + commands: + - python3 -c "raise Exception('boom')" + +tasks: + - id: log + type: io.kestra.plugin.core.log.Log + message: "Triggered with exitCode={{ trigger.exitCode }} (condition={{ trigger.condition }})" +``` + +These triggers support: + +- `interval` to control how often the Python code runs +- `exitCondition` to match an exit code such as `exit 1`, or a regex or substring matched against emitted vars and failure logs +- `edge` to emit only when the condition changes from not matching to matching + +Use these trigger types when you want Python itself to decide whether a polling condition has been met, rather than relying on a separate external-system trigger. + ## Execute GraalVM Task diff --git a/src/contents/docs/15.how-to-guides/shell/index.md b/src/contents/docs/15.how-to-guides/shell/index.md index 5af0122d505..9c6d3b7efc6 100644 --- a/src/contents/docs/15.how-to-guides/shell/index.md +++ b/src/contents/docs/15.how-to-guides/shell/index.md @@ -170,3 +170,57 @@ tasks: Once this has executed, both the metrics can be viewed under **Metrics**. ![metrics](./metrics.png) + +## Automate Shell with triggers + +You can also use shell code as polling logic by using `ScriptTrigger` or `CommandsTrigger`. These trigger types run shell code on an interval and start a flow execution only when the `exitCondition` matches. + +Use `ScriptTrigger` for inline shell code: + +```yaml +id: script_trigger +namespace: company.team + +triggers: + - id: script_failure + type: io.kestra.plugin.scripts.shell.ScriptTrigger + interval: PT10S + exitCondition: "exit 1" + edge: true + containerImage: ubuntu + script: | + cat /path/that/does/not/exist + +tasks: + - id: log + type: io.kestra.plugin.core.log.Log + message: "Triggered with exitCode={{ trigger.exitCode }} (condition={{ trigger.condition }})" +``` + +Use `CommandsTrigger` when you want to run shell commands instead: + +```yaml +id: commands_trigger +namespace: company.team + +triggers: + - id: commands_failure + type: io.kestra.plugin.scripts.shell.CommandsTrigger + interval: PT10S + exitCondition: "exit 1" + edge: true + containerImage: ubuntu + commands: + - cat /path/that/does/not/exist + +tasks: + - id: log + type: io.kestra.plugin.core.log.Log + message: "Triggered with exitCode={{ trigger.exitCode }} (condition={{ trigger.condition }})" +``` + +These trigger types support: + +- `interval` to control how often the script or commands run +- `exitCondition` to match an exit code such as `exit 1`, or a regex or substring matched against emitted vars and failure logs +- `edge` to emit only on a transition from not matching to matching diff --git a/src/contents/docs/16.scripts/index.mdx b/src/contents/docs/16.scripts/index.mdx index 08423e318f8..43fc853ab23 100644 --- a/src/contents/docs/16.scripts/index.mdx +++ b/src/contents/docs/16.scripts/index.mdx @@ -29,8 +29,10 @@ There are dedicated plugins for `Python`, `R`, `Julia`, `Ruby`, `Node.js`, `Powe By default, these tasks run in individual Docker containers (taskRunner type: `io.kestra.plugin.scripts.runner.docker.Docker`). You can overwrite that default behavior if you prefer that your scripts run in a local process (taskRunner type: `io.kestra.plugin.core.runner.Process`) instead. +Some script plugins also support polling triggers. Python, Shell, Ruby, Go, and Node provide `ScriptTrigger` and `CommandsTrigger` types that run code on an interval and start a flow execution only when an `exitCondition` matches. Read more in the [Polling Trigger guide](../05.workflow-components/07.triggers/04.polling-trigger/index.md). + If you use the [Enterprise Edition](../07.enterprise/index.mdx), you can also run your scripts on [dedicated remote workers](../07.enterprise/04.scalability/worker-group/index.md) by specifying a `workerGroup` property or using other [Task Runner types](../task-runners/04.types/index.mdx) for AWS, GCP, Azure, and Kubernetes. The following pages dive into details of each task runner, supported programming languages, and how to manage dependencies. - \ No newline at end of file + From 1f4831618cc74a51ca68ec1c6330643dba45e13d Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Tue, 31 Mar 2026 11:25:30 +0200 Subject: [PATCH 07/52] docs(git-blueprints): add Blueprint Sync and Push docs Part of https://github.com/kestra-io/kestra-ee/issues/7133 --- .../02.governance/custom-blueprints/index.md | 88 +++++++++++++++++++ .../docs/version-control-cicd/04.git/index.md | 11 +++ 2 files changed, 99 insertions(+) diff --git a/src/contents/docs/07.enterprise/02.governance/custom-blueprints/index.md b/src/contents/docs/07.enterprise/02.governance/custom-blueprints/index.md index b2bbc04a5b9..7ba54a74c36 100644 --- a/src/contents/docs/07.enterprise/02.governance/custom-blueprints/index.md +++ b/src/contents/docs/07.enterprise/02.governance/custom-blueprints/index.md @@ -226,3 +226,91 @@ pluginDefaults: password: '{{ secret("ORACLE_USERNAME") }}' ``` ::: + +## Version control for Custom Blueprints + +Custom Blueprints can be version-controlled with Git using two dedicated tasks from the `plugin-ee-git` plugin: + +- [PushBlueprints](/plugins/plugin-ee-git/io.kestra.plugin.ee.git.PushBlueprints) commits and pushes blueprints from Kestra to a Git repository. +- [SyncBlueprints](/plugins/plugin-ee-git/io.kestra.plugin.ee.git.SyncBlueprints) syncs blueprints from a Git repository into Kestra, treating Git as the single source of truth. + +These tasks mirror the [PushFlows and SyncFlows patterns](../../../version-control-cicd/04.git/index.md) used for flows, applied to Custom Blueprints. + +### Push blueprints to Git + +Use `PushBlueprints` to export your blueprints from Kestra into a Git repository. This is useful for creating backups, reviewing changes via pull requests, or promoting blueprints across environments. + +Each blueprint is written as a YAML file to the target `gitDirectory` (default: `_blueprints`). Use the `blueprints` property with glob patterns to push only a subset of blueprints. + +```yaml +id: push_blueprints +namespace: system + +tasks: + - id: commit_and_push + type: io.kestra.plugin.ee.git.PushBlueprints + url: https://github.com/your-org/blueprints-repo + username: git_username + password: "{{ secret('GITHUB_ACCESS_TOKEN') }}" + branch: main + commitMessage: "push blueprints from {{ flow.namespace ~ '.' ~ flow.id }}" + +triggers: + - id: schedule + type: io.kestra.plugin.core.trigger.Schedule + cron: "0 * * * *" +``` + +The task outputs a `commitId`, a `commitURL`, and a `blueprints` URI pointing to a diff report that lists the number of lines added, deleted, and changed per file. + +### Sync blueprints from Git + +Use `SyncBlueprints` to pull blueprints from Git into Kestra. This is the recommended pattern when Git is your single source of truth — for example, when platform teams manage approved blueprint libraries centrally and deploy them across multiple Kestra instances. + +By default, `SyncBlueprints` only adds and updates blueprints. Set `delete: true` to also remove any blueprints present in Kestra but absent in Git. + +```yaml +id: sync_blueprints_from_git +namespace: system + +tasks: + - id: git + type: io.kestra.plugin.ee.git.SyncBlueprints + url: https://github.com/your-org/blueprints-repo + branch: main + username: git_username + password: "{{ secret('GITHUB_ACCESS_TOKEN') }}" + delete: true + dryRun: true + +triggers: + - id: every_full_hour + type: io.kestra.plugin.core.trigger.Schedule + cron: "0 * * * *" +``` + +Set `dryRun: true` to preview what would change without applying it. The `blueprints` output URI contains a row-per-blueprint report showing each blueprint's `syncState`: `ADDED`, `UPDATED`, `UNCHANGED`, or `DELETED`. + +Use caution with `delete: true` — it removes all blueprints not present in Git, not just those that differ. + +### Blueprint YAML file format + +Both tasks read and write blueprints as YAML files. Each file represents one blueprint: + +```yaml +id: my-blueprint-id +title: My Blueprint Title +description: Optional description of what this blueprint does +tags: + - tag1 + - tag2 +flow: | + id: my-flow + namespace: company.team + tasks: + - id: hello + type: io.kestra.plugin.core.log.Log + message: Hello World +``` + +The `id` field controls how blueprints are matched on sync: if a blueprint with that ID already exists in Kestra, it is updated; if not, it is created with that ID. If `id` is omitted, a new blueprint is created with an auto-generated ID. diff --git a/src/contents/docs/version-control-cicd/04.git/index.md b/src/contents/docs/version-control-cicd/04.git/index.md index 04c2983fcaf..e07368bfbe3 100644 --- a/src/contents/docs/version-control-cicd/04.git/index.md +++ b/src/contents/docs/version-control-cicd/04.git/index.md @@ -22,6 +22,8 @@ There are multiple ways to combine Kestra with Git: - [SyncNamespaceFiles](/plugins/plugin-git/io.kestra.plugin.git.syncnamespacefiles) syncs namespace files the same way. - [PushFlows](/plugins/plugin-git/io.kestra.plugin.git.PushFlows) commits and pushes flow edits from the UI to Git, useful when you rely on the built-in editor but still want version history. - [PushNamespaceFiles](/plugins/plugin-git/io.kestra.plugin.git.pushnamespacefiles) does the same for namespace files. +- [SyncBlueprints](/plugins/plugin-ee-git/io.kestra.plugin.ee.git.SyncBlueprints) syncs Custom Blueprints from Git to Kestra (Enterprise Edition). +- [PushBlueprints](/plugins/plugin-ee-git/io.kestra.plugin.ee.git.PushBlueprints) commits and pushes Custom Blueprints from Kestra to Git (Enterprise Edition). - [Clone](https://kestra.io/plugins/git/io.kestra.plugin.git.clone) clones a repository directly into a flow so scripts are available at runtime. - [TenantSync](/plugins/plugin-git/io.kestra.plugin.git.tenantsync) synchronizes all namespaces in a tenant, including flows, files, apps, tests, and dashboards. - [NamespaceSync](/plugins/plugin-git/io.kestra.plugin.git.namespacesync) keeps a single namespace in sync with a Git repo. @@ -189,6 +191,15 @@ The [Git Clone](/plugins/plugin-git/io.kestra.plugin.git.clone) pattern clones a - Infrastructure deployments via [Terraform CLI](/plugins/plugin-terraform/cli/io.kestra.plugin.terraform.cli.terraformcli) or [Ansible CLI](/plugins/plugin-ansible/cli/io.kestra.plugin.ansible.cli.ansiblecli) - Docker builds via the [Docker Build task](/plugins/plugin-docker/io.kestra.plugin.docker.build) +## Git SyncBlueprints and PushBlueprints + +[SyncBlueprints](/plugins/plugin-ee-git/io.kestra.plugin.ee.git.SyncBlueprints) and [PushBlueprints](/plugins/plugin-ee-git/io.kestra.plugin.ee.git.PushBlueprints) bring the same GitOps patterns to Custom Blueprints (Enterprise Edition). + +- **`SyncBlueprints`** – pulls blueprints from a Git directory into Kestra. Use this when a central team owns an approved blueprint library and needs to deploy it across environments. Set `delete: true` to treat Git as the sole source of truth and remove any blueprints not present in the repository. +- **`PushBlueprints`** – exports blueprints from Kestra to Git. Use this when teams author blueprints in the UI and want version history or a pull request review workflow. + +See [Custom Blueprints](../07.enterprise/02.governance/custom-blueprints/index.md#version-control-for-custom-blueprints) for full examples and the blueprint YAML file format. + ## Git TenantSync and NamespaceSync Both [Git TenantSync](/plugins/plugin-git/io.kestra.plugin.git.tenantsync) and [Git NamespaceSync](/plugins/plugin-git/io.kestra.plugin.git.namespacesync) give you full control over synchronizing Kestra objects with your Git repository. From 984f3d080638333fa322b5b60e20f332521ee436 Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Tue, 31 Mar 2026 12:28:58 +0200 Subject: [PATCH 08/52] docs(PurgeLogs): add new flags Part of https://github.com/kestra-io/kestra-ee/issues/6938 --- .../10.administrator-guide/purge/index.md | 46 ++++++++++++++++++- .../11.purging-data/index.md | 3 +- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/contents/docs/10.administrator-guide/purge/index.md b/src/contents/docs/10.administrator-guide/purge/index.md index 067d0f8453f..55c9cdbf8fe 100644 --- a/src/contents/docs/10.administrator-guide/purge/index.md +++ b/src/contents/docs/10.administrator-guide/purge/index.md @@ -11,7 +11,7 @@ Use purge tasks to remove old executions, logs, and key-value pairs, helping red To keep storage optimized, use [`io.kestra.plugin.core.execution.PurgeExecutions`](/plugins/core/tasks/io.kestra.plugin.core.execution.purgeexecutions), [`io.kestra.plugin.core.log.PurgeLogs`](/plugins/core/tasks/log/io.kestra.plugin.core.log.purgelogs), and [`io.kestra.plugin.core.kv.PurgeKV`](/plugins/core/kv/io.kestra.plugin.core.kv.purgekv). - `PurgeExecutions`: deletes execution records -- `PurgeLogs`: removes both `Execution` and `Trigger` logs in bulk +- `PurgeLogs`: removes logs in bulk. By default removes both execution logs and non-execution logs (e.g. trigger logs). Use `purgeExecutionLogs` and `purgeNonExecutionLogs` to purge each type independently. - `PurgeKV`: deletes expired keys globally for a specific namespace Together, these replace the legacy `io.kestra.plugin.core.storage.Purge` task with a **faster and more reliable process (~10x faster)**. @@ -45,6 +45,50 @@ triggers: cron: "@daily" ``` +### Selectively purge execution or trigger logs + +By default, `PurgeLogs` removes all log types. Set `purgeExecutionLogs` or `purgeNonExecutionLogs` to `false` to restrict which logs are deleted. This is useful when you want to retain execution logs for failed runs for debugging while still clearing trigger logs. + +Purge only trigger (non-execution) logs: + +```yaml +id: purge-trigger-logs +namespace: company.myteam + +tasks: + - id: purge_logs + type: io.kestra.plugin.core.log.PurgeLogs + endDate: "{{ now() | dateAdd(-1, 'MONTHS') }}" + purgeExecutionLogs: false + purgeNonExecutionLogs: true + +triggers: + - id: daily + type: io.kestra.plugin.core.trigger.Schedule + cron: "@daily" +``` + +Purge only execution logs: + +```yaml +id: purge-execution-logs +namespace: company.myteam + +tasks: + - id: purge_logs + type: io.kestra.plugin.core.log.PurgeLogs + endDate: "{{ now() | dateAdd(-1, 'MONTHS') }}" + purgeExecutionLogs: true + purgeNonExecutionLogs: false + +triggers: + - id: daily + type: io.kestra.plugin.core.trigger.Schedule + cron: "@daily" +``` + +The task outputs `executionLogsCount` and `nonExecutionLogsCount` alongside the existing `count` (total), so you can log or alert on how many of each type were removed. + ## Purge Key-value pairs The example below purges expired Key-value pairs from the `company` Namespace. It's set up as a flow in the [`system`](../../06.concepts/system-flows/index.md) Namespace. diff --git a/src/contents/docs/14.best-practices/11.purging-data/index.md b/src/contents/docs/14.best-practices/11.purging-data/index.md index e34171b5182..e5ac1eb1363 100644 --- a/src/contents/docs/14.best-practices/11.purging-data/index.md +++ b/src/contents/docs/14.best-practices/11.purging-data/index.md @@ -33,7 +33,7 @@ Use this rule of thumb: | If you want to remove... | Prefer | Why | | --- | --- | --- | | Old execution records | [`PurgeExecutions`](/plugins/core/tasks/io.kestra.plugin.core.execution.purgeexecutions) | It permanently deletes execution metadata and related execution data | -| Old execution and trigger logs | [`PurgeLogs`](/plugins/core/tasks/log/io.kestra.plugin.core.log.purgelogs) | It is designed for bulk log cleanup | +| Old execution logs, trigger logs, or both | [`PurgeLogs`](/plugins/core/tasks/log/io.kestra.plugin.core.log.purgelogs) | Use `purgeExecutionLogs` and `purgeNonExecutionLogs` to target each type independently, or leave both `true` (default) to purge all logs | | Expired runtime state in the KV Store | [`PurgeKV`](/plugins/core/kv/io.kestra.plugin.core.kv.purgekv) or automatic KV expiration purge | It removes stale KV entries without treating them as static configuration | | Old Namespace file versions | [`PurgeFiles`](/plugins/core/namespace/io.kestra.plugin.core.namespace.purgefiles) | It applies retention rules to Namespace files and their versions | | Old asset records, usages, or lineage data | [`PurgeAssets`](../../10.administrator-guide/purge/index.md#purge-assets-and-lineage-retention) | It applies retention to asset-related records without touching executions or logs | @@ -65,6 +65,7 @@ This is usually the right choice when: Best practice: - set separate retention periods for executions and logs if your teams use them differently +- use `purgeExecutionLogs: false` to retain execution logs for failed workflow debugging while still purging trigger logs, or `purgeNonExecutionLogs: false` to do the reverse - avoid deleting recent data that is still useful for troubleshooting failed workflows - run purge flows on a schedule instead of waiting for storage pressure From 1b003713c2ac2ef4a0616d900ef2364f6726d23e Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Tue, 31 Mar 2026 15:04:15 +0200 Subject: [PATCH 09/52] docs(agent-skills): add tools section and skill example Part of https://github.com/kestra-io/plugin-ai/issues/266 --- src/contents/docs/ai-tools/ai-agents/index.md | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/src/contents/docs/ai-tools/ai-agents/index.md b/src/contents/docs/ai-tools/ai-agents/index.md index d2eff67cda8..e843eefe8d3 100644 --- a/src/contents/docs/ai-tools/ai-agents/index.md +++ b/src/contents/docs/ai-tools/ai-agents/index.md @@ -124,3 +124,109 @@ These outputs can then be passed on as notifications or system messages to exter ### Plugin defaults Each task using the AI Agent requires the `provider` property. To avoid repetition and simplify the flow building experience, first consider using [Kestra's AI Copilot](../ai-copilot/index.md), next consider using [Plugin Defaults](../../05.workflow-components/09.plugin-defaults/index.md) to ensure consistency and remove repetition. Additionally, for your provider API key, make sure to secure it either through the [Key-Value Store](../../06.concepts/05.kv-store/index.md) or as a [Secret](../../06.concepts/04.secret/index.md) if using [Kestra Enterprise Edition](../../07.enterprise/01.overview/01.enterprise-edition/index.md). + +## Agent tools + +The AI Agent can be extended with **tools** — capabilities the LLM can choose to invoke at runtime to complete its task. Tools are listed under the `tools` property of an `AIAgent` task. + +### Skills + +The [**Skill**](/plugins/plugin-ai/tool/skill) tool lets you attach structured instructions to an agent that it can activate on demand. Rather than including all instructions in the system message, skills let you define discrete, reusable knowledge blocks — each with a name, a description the LLM uses to decide when to activate it, and the actual instruction content. + +This is useful when an agent has multiple possible modes of operation, such as translating text, reviewing code, or formatting data, where you want the LLM to select and apply the right instructions based on context rather than always receiving all instructions at once. + +Each skill requires: +- `name` — a unique identifier for the skill +- `description` — explains to the LLM when to activate the skill +- `content` or `contentUri` — the instruction content, either inline or loaded from Kestra internal storage + +#### Inline skill content + +The simplest way to define a skill is with inline `content`: + +```yaml +id: agent_with_skills +namespace: company.ai + +tasks: + - id: agent + type: io.kestra.plugin.ai.agent.AIAgent + prompt: Translate the following text to French - "Hello, how are you today?" + provider: + type: io.kestra.plugin.ai.provider.GoogleGemini + modelName: gemini-2.5-flash + apiKey: "{{ secret('GEMINI_API_KEY') }}" + tools: + - type: io.kestra.plugin.ai.tool.Skill + skills: + - name: translation_expert + description: Expert translator for multiple languages + content: | + You are an expert translator. When translating text: + 1. Preserve the original meaning and tone + 2. Use natural phrasing in the target language + 3. Keep proper nouns unchanged +``` + +#### Loading skill content from storage + +For longer or reusable instructions, store the skill content as a file in Kestra internal storage and reference it with `contentUri`. This is especially useful when skill content is generated or updated by an earlier task in the same flow: + +```yaml +id: agent_with_skill_from_storage +namespace: company.ai + +tasks: + - id: write_instructions + type: io.kestra.plugin.core.storage.Write + content: | + You are a senior code reviewer. When reviewing code: + 1. Check for security vulnerabilities + 2. Ensure proper error handling + 3. Verify naming conventions are followed + 4. Flag any code duplication + + - id: agent + type: io.kestra.plugin.ai.agent.AIAgent + prompt: Review this Python function - "def add(a, b): return a + b" + provider: + type: io.kestra.plugin.ai.provider.GoogleGemini + modelName: gemini-2.5-flash + apiKey: "{{ secret('GEMINI_API_KEY') }}" + tools: + - type: io.kestra.plugin.ai.tool.Skill + skills: + - name: code_review_expert + description: Expert code reviewer with strict guidelines + contentUri: "{{ outputs.write_instructions.uri }}" +``` + +A single `Skill` tool can define multiple skills. Each skill must have a unique name. `content` and `contentUri` are mutually exclusive — exactly one must be set per skill. For more details on all available properties, refer to the [Skill plugin documentation](/plugins/plugin-ai/tool/skill). + +### Kestra-native tools + +- [**KestraFlow**](/plugins/plugin-ai/tool/kestraflow) — triggers a Kestra flow as a tool, either with a predefined namespace and flow ID or dynamically based on the agent's prompt. +- [**KestraTask**](/plugins/plugin-ai/tool/kestratask) — exposes one or more Kestra runnable tasks as tools, letting the agent supply values for properties left unset. + +### Web search + +- [**TavilyWebSearch**](/plugins/plugin-ai/tool/tavilywebsearch) — gives the agent access to live web results via the Tavily search API. +- [**GoogleCustomWebSearch**](/plugins/plugin-ai/tool/googlecustomwebsearch) — gives the agent access to live web results via a Google Custom Search Engine. + +### Code execution + +- [**CodeExecution**](/plugins/plugin-ai/tool/codeexecution) — lets the agent write and run JavaScript snippets in a Judge0 sandbox (via RapidAPI). + +### Nested agents + +- [**AIAgent**](/plugins/plugin-ai/tool/aiagent) — wraps another AI agent as a callable tool so a parent agent can delegate sub-tasks to a specialized child agent. +- [**A2AClient**](/plugins/plugin-ai/tool/a2aclient) — forwards prompts to a remote AI agent over the Agent-to-Agent (A2A) protocol and returns its response. + +### MCP clients + +Connect the agent to any [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server to expose its tools: + +- [**DockerMcpClient**](/plugins/plugin-ai/tool/dockermcpclient) — runs an MCP server inside a Docker container. +- [**SseMcpClient**](/plugins/plugin-ai/tool/ssemcpclient) — connects to a remote MCP server over Server-Sent Events (SSE). +- [**StdioMcpClient**](/plugins/plugin-ai/tool/stdiomcpclient) — spawns a local MCP server process and communicates over stdio. +- [**StreamableHttpMcpClient**](/plugins/plugin-ai/tool/streamablehttpmcpclient) — connects to an MCP server over HTTP streaming. From 89cf5cd63bdd5d0fdc4529369ea966542acf19a6 Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Thu, 2 Apr 2026 16:00:56 +0200 Subject: [PATCH 10/52] docs(allowed-namespaces): match current ui Part of https://github.com/kestra-io/kestra-ee/issues/6833 --- .../02.governance/07.namespace-management/index.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/contents/docs/07.enterprise/02.governance/07.namespace-management/index.md b/src/contents/docs/07.enterprise/02.governance/07.namespace-management/index.md index 608656c9a67..74efb8b59e4 100644 --- a/src/contents/docs/07.enterprise/02.governance/07.namespace-management/index.md +++ b/src/contents/docs/07.enterprise/02.governance/07.namespace-management/index.md @@ -306,10 +306,7 @@ kestra_password = "your-kestra-password" ``` ## Allowed Namespaces -When you navigate to any Namespace and go to the Edit tab, you can explicitly configure which Namespaces are allowed to access flows and other resources related to that Namespace. By default, all Namespaces are allowed: -![allowed-namespaces](./allowed-namespaces.png) +When you navigate to any Namespace and go to the **Edit** tab, you can explicitly configure which Namespaces are allowed to access flows and other resources related to that Namespace. -However, you can restrict that access if you want only specific Namespaces (or no Namespace at all) to trigger its corresponding resources. - -![allowed-namespaces-2](./allowed-namespaces-2.png) +By default, **all Namespaces** are allowed. To restrict access, **select specific Namespaces** — access automatically extends to each selected namespace's children. From 4c7f58c85e9713bfb60bc3c4181da2ab31cd48d1 Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Mon, 13 Apr 2026 12:34:20 +0200 Subject: [PATCH 11/52] docs(PurgeLogs): add batch size --- .../10.administrator-guide/purge/index.md | 26 ++++++++++++++++--- .../11.purging-data/index.md | 1 + 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/contents/docs/10.administrator-guide/purge/index.md b/src/contents/docs/10.administrator-guide/purge/index.md index 55c9cdbf8fe..5537d50bd33 100644 --- a/src/contents/docs/10.administrator-guide/purge/index.md +++ b/src/contents/docs/10.administrator-guide/purge/index.md @@ -11,7 +11,7 @@ Use purge tasks to remove old executions, logs, and key-value pairs, helping red To keep storage optimized, use [`io.kestra.plugin.core.execution.PurgeExecutions`](/plugins/core/tasks/io.kestra.plugin.core.execution.purgeexecutions), [`io.kestra.plugin.core.log.PurgeLogs`](/plugins/core/tasks/log/io.kestra.plugin.core.log.purgelogs), and [`io.kestra.plugin.core.kv.PurgeKV`](/plugins/core/kv/io.kestra.plugin.core.kv.purgekv). - `PurgeExecutions`: deletes execution records -- `PurgeLogs`: removes logs in bulk. By default removes both execution logs and non-execution logs (e.g. trigger logs). Use `purgeExecutionLogs` and `purgeNonExecutionLogs` to purge each type independently. +- `PurgeLogs`: removes execution logs and non-execution logs (e.g. trigger logs) in bulk; use `purgeExecutionLogs` and `purgeNonExecutionLogs` to target each type independently - `PurgeKV`: deletes expired keys globally for a specific namespace Together, these replace the legacy `io.kestra.plugin.core.storage.Purge` task with a **faster and more reliable process (~10x faster)**. @@ -47,7 +47,7 @@ triggers: ### Selectively purge execution or trigger logs -By default, `PurgeLogs` removes all log types. Set `purgeExecutionLogs` or `purgeNonExecutionLogs` to `false` to restrict which logs are deleted. This is useful when you want to retain execution logs for failed runs for debugging while still clearing trigger logs. +Both `purgeExecutionLogs` and `purgeNonExecutionLogs` default to `true`. Set either to `false` to exclude that log type — for example, to retain execution logs for debugging while still clearing trigger logs. Purge only trigger (non-execution) logs: @@ -60,7 +60,6 @@ tasks: type: io.kestra.plugin.core.log.PurgeLogs endDate: "{{ now() | dateAdd(-1, 'MONTHS') }}" purgeExecutionLogs: false - purgeNonExecutionLogs: true triggers: - id: daily @@ -78,7 +77,6 @@ tasks: - id: purge_logs type: io.kestra.plugin.core.log.PurgeLogs endDate: "{{ now() | dateAdd(-1, 'MONTHS') }}" - purgeExecutionLogs: true purgeNonExecutionLogs: false triggers: @@ -89,6 +87,26 @@ triggers: The task outputs `executionLogsCount` and `nonExecutionLogsCount` alongside the existing `count` (total), so you can log or alert on how many of each type were removed. +### Control deletion batch size + +By default, `PurgeLogs` deletes all matching rows in a single transaction. Use `batchSize` to split the deletion into smaller batches — useful when purging a large volume of logs to limit transaction size: + +```yaml +id: purge-logs-batched +namespace: company.myteam + +tasks: + - id: purge_logs + type: io.kestra.plugin.core.log.PurgeLogs + endDate: "{{ now() | dateAdd(-1, 'MONTHS') }}" + batchSize: 1000 + +triggers: + - id: daily + type: io.kestra.plugin.core.trigger.Schedule + cron: "@daily" +``` + ## Purge Key-value pairs The example below purges expired Key-value pairs from the `company` Namespace. It's set up as a flow in the [`system`](../../06.concepts/system-flows/index.md) Namespace. diff --git a/src/contents/docs/14.best-practices/11.purging-data/index.md b/src/contents/docs/14.best-practices/11.purging-data/index.md index e5ac1eb1363..fd9c67f091f 100644 --- a/src/contents/docs/14.best-practices/11.purging-data/index.md +++ b/src/contents/docs/14.best-practices/11.purging-data/index.md @@ -66,6 +66,7 @@ Best practice: - set separate retention periods for executions and logs if your teams use them differently - use `purgeExecutionLogs: false` to retain execution logs for failed workflow debugging while still purging trigger logs, or `purgeNonExecutionLogs: false` to do the reverse +- set `batchSize` on `PurgeLogs` when purging large volumes of logs to limit the number of rows deleted per transaction - avoid deleting recent data that is still useful for troubleshooting failed workflows - run purge flows on a schedule instead of waiting for storage pressure From 56cc53ddbbf25f3219116be7eb971e189fd029c3 Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Mon, 13 Apr 2026 14:51:00 +0200 Subject: [PATCH 12/52] docs(executor-metrics): add new metrics Part of https://github.com/kestra-io/kestra-ee/issues/6471 --- .../docs/10.administrator-guide/prometheus-metrics/index.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/contents/docs/10.administrator-guide/prometheus-metrics/index.md b/src/contents/docs/10.administrator-guide/prometheus-metrics/index.md index 47f6054e465..b8169cf5271 100644 --- a/src/contents/docs/10.administrator-guide/prometheus-metrics/index.md +++ b/src/contents/docs/10.administrator-guide/prometheus-metrics/index.md @@ -43,6 +43,12 @@ Executor server exclusive: * `kestra_executor_execution_message_process_seconds_max` (gauge): Maximum observed duration of a single execution message processed by the Executor. * `kestra_executor_execution_started_count_total` (counter): The total number of executions started by the Executor. * `kestra_executor_flowable_execution_count_total` (counter): The total number of flowable tasks executed by the Executor +* `kestra_executor_loop_delay_duration_seconds` (summary): Execution delay loop duration inside the Executor. +* `kestra_executor_loop_delay_duration_seconds_max` (gauge): Maximum observed execution delay loop duration inside the Executor. +* `kestra_executor_loop_sla_duration_seconds` (summary): SLA monitor loop duration inside the Executor. +* `kestra_executor_loop_sla_duration_seconds_max` (gauge): Maximum observed SLA monitor loop duration inside the Executor. +* `kestra_executor_processing_flow_trigger_duration_seconds` (summary): Flow trigger processing duration inside the Executor. +* `kestra_executor_processing_flow_trigger_duration_seconds_max` (gauge): Maximum observed flow trigger processing duration inside the Executor. * `kestra_executor_taskrun_created_count_total` (counter): The total number of tasks created by the Executor. * `kestra_executor_taskrun_ended_count_total` (counter): he total number of tasks ended by the Executor. * `kestra_executor_taskrun_ended_duration_seconds` (summary): Task duration inside the Executor. From b1200099a946fca112b302d08594c057ce03cde1 Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Mon, 13 Apr 2026 08:35:43 -0500 Subject: [PATCH 13/52] docs(expressions): bring back the monolith page (#4564) * docs(expressions): bring back the monolith page * docs(expressions): add more top level navigation * Update index.mdx * Update index.mdx * docs(expressions): add missing expressions after source code deep dive * Update index.mdx --- .../blogs/2024-04-12-release-0-16/index.md | 4 +- .../04.variables/index.md | 2 +- .../06.outputs/index.md | 2 +- .../04.scalability/apps/index.md | 2 +- .../json-objects-serialization/index.md | 4 +- .../9.secrets-management/index.md | 2 +- .../15.how-to-guides/http-request/index.md | 2 +- .../expressions/01.execution-context/index.md | 194 -- .../expressions/02.pebble-syntax/index.md | 188 -- .../expressions/03.filter-reference/index.md | 594 ------ .../04.function-reference/index.md | 299 --- .../05.operators-tags-tests/index.md | 346 ---- src/contents/docs/expressions/index.mdx | 1721 ++++++++++++++++- src/contents/redirects/docs.yml | 10 + 14 files changed, 1723 insertions(+), 1647 deletions(-) delete mode 100644 src/contents/docs/expressions/01.execution-context/index.md delete mode 100644 src/contents/docs/expressions/02.pebble-syntax/index.md delete mode 100644 src/contents/docs/expressions/03.filter-reference/index.md delete mode 100644 src/contents/docs/expressions/04.function-reference/index.md delete mode 100644 src/contents/docs/expressions/05.operators-tags-tests/index.md diff --git a/src/contents/blogs/2024-04-12-release-0-16/index.md b/src/contents/blogs/2024-04-12-release-0-16/index.md index f47c0848b63..598ff22db8f 100644 --- a/src/contents/blogs/2024-04-12-release-0-16/index.md +++ b/src/contents/blogs/2024-04-12-release-0-16/index.md @@ -170,7 +170,7 @@ Note how in this example, the `waitForCompletion` property is templated using Pe ### New pebble functions to process YAML -Related to the templated task, there are [new Pebble functions](../../docs/expressions/03.filter-reference/index.md#yaml-filters) to process YAML including the `yaml` and `indent` functions that allow you to parse and load YAML strings into objects. Those objects can then be further transformed using Pebble templating. +Related to the templated task, there are [new Pebble functions](../../docs/expressions/index.mdx#yaml-filters) to process YAML including the `yaml` and `indent` functions that allow you to parse and load YAML strings into objects. Those objects can then be further transformed using Pebble templating. Big thanks to [kriko](https://github.com/kriko) for [contributing this feature](https://github.com/kestra-io/kestra/pull/3283)! @@ -209,7 +209,7 @@ The above flow will only be triggered after an execution: ## Improvements to the secret function -The `secret()` function now returns `null` if the secret cannot be found. [This change](https://github.com/kestra-io/kestra/issues/3162) allows you to fall back to an environment variable if a secret is missing. To do that, you can use the `secret()` function in combination with the [null-coalescing](../../docs/expressions/05.operators-tags-tests/index.md#fallbacks-and-conditionals) operator as follows: +The `secret()` function now returns `null` if the secret cannot be found. [This change](https://github.com/kestra-io/kestra/issues/3162) allows you to fall back to an environment variable if a secret is missing. To do that, you can use the `secret()` function in combination with the [null-coalescing](../../docs/expressions/index.mdx#fallbacks-and-conditionals) operator as follows: ```yaml accessKeyId: "{{ secret('AWS_ACCESS_KEY_ID') ?? env.aws_access_key_id }}" diff --git a/src/contents/docs/05.workflow-components/04.variables/index.md b/src/contents/docs/05.workflow-components/04.variables/index.md index 0478acef1d8..d4e2ce2fd20 100644 --- a/src/contents/docs/05.workflow-components/04.variables/index.md +++ b/src/contents/docs/05.workflow-components/04.variables/index.md @@ -138,7 +138,7 @@ After executing the flow, the only remaining variable is `nested.unchanged` with ### 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/05.operators-tags-tests/index.md). For example, the following returns the string `{{ myvar }}` instead of the value of `myvar`: +To ensure that a block of code won't be parsed by Pebble, you can use the `{% raw %}` and `{% endraw %}` [Pebble tags](../../expressions/index.mdx#raw). For example, the following returns the string `{{ myvar }}` instead of the value of `myvar`: ```yaml {% raw %}{{ myvar }}{% endraw %} diff --git a/src/contents/docs/05.workflow-components/06.outputs/index.md b/src/contents/docs/05.workflow-components/06.outputs/index.md index 4876bcc760c..9671b0875ac 100644 --- a/src/contents/docs/05.workflow-components/06.outputs/index.md +++ b/src/contents/docs/05.workflow-components/06.outputs/index.md @@ -357,7 +357,7 @@ tasks: message: "{{ outputs.second['value 1'].values.data }}" ``` -You can also use the `currentEachOutput` function to access the current tree task. See [Function Reference](../../expressions/04.function-reference/index.md) for more details. +You can also use the `currentEachOutput` function to access the current tree task. See [Function Reference](../../expressions/index.mdx#function-reference) for more details. If you need the output from a previous iteration of the same task, or from a sibling task in a previous iteration, use `iterationOutput()` instead. diff --git a/src/contents/docs/07.enterprise/04.scalability/apps/index.md b/src/contents/docs/07.enterprise/04.scalability/apps/index.md index 1cbbd35a482..ac743f46eb8 100644 --- a/src/contents/docs/07.enterprise/04.scalability/apps/index.md +++ b/src/contents/docs/07.enterprise/04.scalability/apps/index.md @@ -280,7 +280,7 @@ You can copy the URL from the Apps catalog page in the Kestra UI. ### App expressions -From within flows, you can generate App URLs using the Enterprise-only `appLink` expression. See the [Function Reference](../../../expressions/04.function-reference/index.md) for parameters and examples. +From within flows, you can generate App URLs using the Enterprise-only `appLink` expression. See the [Function Reference](../../../expressions/index.mdx#function-reference) for parameters and examples. --- diff --git a/src/contents/docs/11.migration-guide/v0.17.0/json-objects-serialization/index.md b/src/contents/docs/11.migration-guide/v0.17.0/json-objects-serialization/index.md index c07c5005aee..533a219264a 100644 --- a/src/contents/docs/11.migration-guide/v0.17.0/json-objects-serialization/index.md +++ b/src/contents/docs/11.migration-guide/v0.17.0/json-objects-serialization/index.md @@ -14,8 +14,8 @@ Kestra 0.17 migrates away from the previously used `NON_DEFAULT` JSON serializat There are three main cases where Pebble expressions might be affected: -1) [Operators, Tags, and Tests](../../../expressions/05.operators-tags-tests/index.md) -2) [Pebble Syntax](../../../expressions/02.pebble-syntax/index.md) +1) [Operators, Tags, and Tests](../../../expressions/index.mdx#operators-tags-and-tests) +2) [Pebble Syntax](../../../expressions/index.mdx#pebble-syntax) 3) [Conditions in Pebble](../../../06.concepts/06.pebble/index.md#using-conditions-in-pebble) ## 0.16 diff --git a/src/contents/docs/14.best-practices/9.secrets-management/index.md b/src/contents/docs/14.best-practices/9.secrets-management/index.md index e47c415bfa4..045666e5441 100644 --- a/src/contents/docs/14.best-practices/9.secrets-management/index.md +++ b/src/contents/docs/14.best-practices/9.secrets-management/index.md @@ -15,7 +15,7 @@ Kestra provides a built-in [secret manager](../../07.enterprise/02.governance/se ## Secret obfuscation in logs is best effort Kestra attempts to mask secrets in logs and during expression evaluation, but masking is not foolproof. -Current log obfuscation replaces full secret matches with `****`. However, if a secret is modified — for example, through substring extraction, concatenation, encoding, or interpolation — it may bypass obfuscation and appear in logs. Refer to the [Filter Reference](../../expressions/03.filter-reference/index.md) for a list of possible transformations. +Current log obfuscation replaces full secret matches with `****`. However, if a secret is modified — for example, through substring extraction, concatenation, encoding, or interpolation — it may bypass obfuscation and appear in logs. Refer to the [Filter Reference](../../expressions/index.mdx#filter-reference) for a list of possible transformations. For example, the following flow uses `jq()` in a log message to return a partial value associated with a secret: diff --git a/src/contents/docs/15.how-to-guides/http-request/index.md b/src/contents/docs/15.how-to-guides/http-request/index.md index 4aec76b6e82..55b8627ddf5 100644 --- a/src/contents/docs/15.how-to-guides/http-request/index.md +++ b/src/contents/docs/15.how-to-guides/http-request/index.md @@ -177,7 +177,7 @@ tasks: We can define the request body as an input so it's easier to remember what it is, change it when we execute and to use in multiple places if we decide to make multiple requests with the same body. :::alert{type="info"} -If your body message input is multiple lines, the best practice is to use a pebble expression to convert it to JSON and avoid escape function issues. For more details, check out this [multiline JSON example with pebble](../../expressions/02.pebble-syntax/index.md#multiline-json-bodies). +If your body message input is multiple lines, the best practice is to use a pebble expression to convert it to JSON and avoid escape function issues. For more details, check out this [multiline JSON example with pebble](../../expressions/index.mdx#multiline-json-bodies). ::: When we execute this as a `POST` request, this is the response we receive using the same Debug Expression option in the Outputs page: diff --git a/src/contents/docs/expressions/01.execution-context/index.md b/src/contents/docs/expressions/01.execution-context/index.md deleted file mode 100644 index a60623c6fe7..00000000000 --- a/src/contents/docs/expressions/01.execution-context/index.md +++ /dev/null @@ -1,194 +0,0 @@ ---- -title: Execution Context Variables in Kestra Expressions -description: Learn which variables are available in Kestra expressions, including flow metadata, inputs, outputs, triggers, namespace values, environment variables, and secrets. -sidebarTitle: Execution Context Variables -icon: /src/contents/docs/icons/expression.svg ---- - -Use this page when you need to know what data is available inside `{{ ... }}` at runtime. - -## Understand the execution context - -Kestra expressions combine the [Pebble templating engine](../../06.concepts/06.pebble/index.md) with the execution context to dynamically render flow properties. - -The execution context usually includes: - -- `flow` -- `execution` -- `inputs` -- `outputs` -- `labels` -- `tasks` -- `trigger` when the flow was started by a trigger -- `vars` when the flow defines variables -- `namespace` in Enterprise Edition when namespace variables are configured -- `envs` for environment variables -- `globals` for global configuration values - -:::alert{type="info"} -To inspect the full runtime context, use `{{ printContext() }}` in the Debug Expression console. -::: - -The Debug Expression console is available in the Kestra UI under **Executions → Logs → Debug Expression**. Enter any expression and evaluate it against the live execution context without modifying the flow. - -
- -
- -## Default execution context variables - -| Parameter | Description | -| --- | --- | -| `{{ flow.id }}` | Identifier of the flow | -| `{{ flow.namespace }}` | Namespace of the flow | -| `{{ flow.tenantId }}` | Tenant identifier in Enterprise Edition | -| `{{ flow.revision }}` | Flow revision number | -| `{{ execution.id }}` | Unique execution identifier | -| `{{ execution.startDate }}` | Start date of the execution | -| `{{ execution.state }}` | Current execution state | -| `{{ execution.originalId }}` | Original execution ID preserved across replays | -| `{{ task.id }}` | Current task identifier | -| `{{ task.type }}` | Fully qualified class name of the current task | -| `{{ taskrun.id }}` | Current task run identifier | -| `{{ taskrun.startDate }}` | Start date of the current task run | -| `{{ taskrun.attemptsCount }}` | Retry and restart attempt count | -| `{{ taskrun.parentId }}` | Parent task run identifier for nested tasks | -| `{{ taskrun.value }}` | Current loop or flowable value | -| `{{ parent.taskrun.value }}` | Value of the nearest parent task run | -| `{{ parent.outputs }}` | Outputs of the nearest parent task run | -| `{{ parents }}` | List of parent task runs | -| `{{ labels }}` | Execution labels accessible by key | - -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") }} -``` - -## Trigger variables - -When the execution is started by a `Schedule` trigger: - -| Parameter | Description | -| --- | --- | -| `{{ trigger.date }}` | Date of the current schedule | -| `{{ trigger.next }}` | Date of the next schedule | -| `{{ trigger.previous }}` | Date of the previous schedule | - -When the execution is started by a `Flow` trigger: - -| Parameter | Description | -| --- | --- | -| `{{ trigger.executionId }}` | ID of the triggering execution | -| `{{ trigger.namespace }}` | Namespace of the triggering flow | -| `{{ trigger.flowId }}` | ID of the triggering flow | -| `{{ trigger.flowRevision }}` | Revision of the triggering flow | - -## Environment and global variables - -Kestra provides access to environment variables prefixed with `ENV_` by default, unless configured otherwise in the [runtime and storage configuration](../../configuration/02.runtime-and-storage/index.md). - -- reference `ENV_FOO` as `{{ envs.foo }}` -- reference the configured environment name as `{{ kestra.environment }}` -- reference the configured Kestra URL as `{{ kestra.url }}` -- reference global variables from configuration as `{{ globals.foo }}` - -## Flow variables and inputs - -Use flow-level variables with `vars.*`: - -```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 }}" -``` - -Use inputs with `inputs.*`: - -```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, credentials, namespace variables, and outputs - -Use `secret()` to inject secret values at runtime: - -```yaml -tasks: - - id: myTask - type: io.kestra.plugin.core.debug.Return - format: "{{ secret('MY_SECRET') }}" -``` - -Use `credential()` in Enterprise Edition to inject a short-lived token from a managed [Credential](../../07.enterprise/03.auth/credentials/index.md): - -```yaml -tasks: - - id: request - type: io.kestra.plugin.core.http.Request - method: GET - uri: https://api.example.com/v1/ping - auth: - type: BEARER - token: "{{ credential('my_oauth') }}" -``` - -`credential()` returns the short-lived token only. The credential itself is managed in the Kestra UI. - -Use namespace variables in Enterprise Edition with `namespace.*`. To set them up: - -1. Open the Kestra UI and navigate to **Namespaces**. -2. Select the namespace where the flow runs. -3. Open the **Variables** tab. -4. Add a key-value pair such as `github.token` with the desired value. - -Reference namespace variables in expressions using dot notation: - -```yaml -format: "{{ namespace.github.token }}" -``` - -If a namespace variable itself contains Pebble, evaluate it with `render()`: - -```yaml -format: "{{ render(namespace.github.token) }}" -``` - -Use outputs with `outputs.taskId.attribute`: - -```yaml -message: | - First: {{ outputs.first.value }} - Second: {{ outputs['second-task'].value }} -``` - -:::alert{type="info"} -If a task ID or output key contains a hyphen, use bracket notation such as `outputs['second-task']`. To avoid that, prefer `camelCase` or `snake_case`. -::: - -For Pebble syntax details, continue with [Pebble Syntax](../02.pebble-syntax/index.md). For runtime helpers, go to [Function Reference](../04.function-reference/index.md). diff --git a/src/contents/docs/expressions/02.pebble-syntax/index.md b/src/contents/docs/expressions/02.pebble-syntax/index.md deleted file mode 100644 index 16536055fc2..00000000000 --- a/src/contents/docs/expressions/02.pebble-syntax/index.md +++ /dev/null @@ -1,188 +0,0 @@ ---- -title: Pebble Syntax in Kestra Expressions -description: Learn the core Pebble templating syntax used in Kestra expressions, including delimiters, attribute access, control structures, macros, and null handling. -sidebarTitle: Pebble Syntax -icon: /src/contents/docs/icons/expression.svg ---- - -Use this page when you need help writing expressions rather than looking up a specific filter or function. - -## Pebble basics - -Pebble templates use two primary delimiters: - -- `{{ ... }}` to output the result of an expression -- `{% ... %}` to control template flow with tags such as `if`, `for`, or `set` - -Examples: - -```twig -{{ flow.id }} -{% if inputs.region == "eu" %}Europe{% endif %} -``` - -To escape Pebble syntax literally, use the `raw` tag described in [Operators, Tags, and Tests](../05.operators-tags-tests/index.md#tags). - -## Accessing values - -Use dot notation for standard property access: - -```twig -{{ foo.bar }} -``` - -Use bracket notation for special characters or indexed access: - -```twig -{{ foo['foo-bar'] }} -{{ items[0] }} -``` - -:::alert{type="warning"} -If a task ID, output key, or attribute contains a hyphen, use bracket notation. To avoid that, prefer `camelCase` or `snake_case`. -::: - -## Parsing nested expressions - -Kestra renders expressions once by default. If a variable contains Pebble that should be evaluated later, use `render()`: - -```yaml -variables: - trigger_or_yesterday: "{{ trigger.date ?? (execution.startDate | dateAdd(-1, 'DAYS')) }}" - input_or_yesterday: "{{ inputs.mydate ?? (execution.startDate | dateAdd(-1, 'DAYS')) }}" - -tasks: - - id: yesterday - type: io.kestra.plugin.core.log.Log - message: "{{ render(vars.trigger_or_yesterday) }}" - - - id: input_or_yesterday - type: io.kestra.plugin.core.log.Log - message: "{{ render(vars.input_or_yesterday) }}" -``` - -This pattern is especially useful with namespace variables, composed flow variables, and fallback logic based on trigger context. - -### Multiline JSON bodies - -When an HTTP request body contains multiline user input, avoid partial string interpolation. Instead, build the whole payload as a single Pebble expression so JSON escaping happens correctly. - -```yaml -id: multiline_input_passed_to_json_body -namespace: company.team - -inputs: - - id: title - type: STRING - defaults: This is my title - - id: message - type: STRING - defaults: |- - This is my long - multiline message. - - id: priority - type: INT - defaults: 5 - -tasks: - - id: hello - type: io.kestra.plugin.core.http.Request - uri: https://kestra.io/api/mock - method: POST - body: | - {{ { - "title": inputs.title, - "message": inputs.message, - "priority": inputs.priority - } | toJson }} -``` - -## Common syntax patterns - -### Comments - -Use Pebble comments with `{# ... #}`: - -```twig -{# This is a comment #} -{{ "Visible content" }} -``` - -In YAML, continue to use `#` for comments outside the expression itself. - -### Literals and collections - -Pebble supports: - -- strings: `"Hello World"` -- numbers such as `100 + 10l * 2.5` -- booleans: `true`, `false` -- null: `null` -- lists: `["apple", "banana"]` -- maps: `{"apple":"red", "banana":"yellow"}` - -### Named arguments - -Filters, functions, and macros can accept named arguments: - -```twig -{{ stringDate | date(existingFormat="yyyy-MMMM-d", format="yyyy/MMMM/d") }} -``` - -### Macros - -Macros are reusable template snippets: - -```twig -{% macro input(type="text", name, value="") %} - type: "{{ type }}", name: "{{ name }}", value: "{{ value }}" -{% endmacro %} - -{{ input(name="country") }} -``` - -Macros only access their local arguments. - -## Control flow and fallbacks - -Common patterns: - -- `if` and `elseif` for branching -- `for` for iteration -- `??` for fallback values -- `? :` for ternary expressions - -Examples: - -```twig -{{ inputs.mydate ?? (execution.startDate | dateAdd(-1, 'DAYS')) }} -``` - -```twig -{% for article in articles %} - {{ article.title }} -{% else %} - No articles available. -{% endfor %} -``` - -Inside a `for` loop, Pebble provides a `loop` object with properties such as `loop.index`, `loop.first`, `loop.last`, and `loop.length`. For the full table and examples, see [Operators, Tags, and Tests](../05.operators-tags-tests/index.md#for). - -```twig -{% if category == "news" %} - {{ news }} -{% elseif category == "sports" %} - {{ sports }} -{% else %} - Select a category -{% endif %} -``` - -For full operator and tag details, see [Operators, Tags, and Tests](../05.operators-tags-tests/index.md). - -## When to leave this page - -- Need available runtime variables: [Execution Context Variables](../01.execution-context/index.md) -- Need filters such as `date`, `jq`, `default`, or `yaml`: [Filter Reference](../03.filter-reference/index.md) -- Need functions such as `render()`, `secret()`, or `printContext()`: [Function Reference](../04.function-reference/index.md) -- Need operator details such as `??`, ternary expressions, tags, or tests: [Operators, Tags, and Tests](../05.operators-tags-tests/index.md) diff --git a/src/contents/docs/expressions/03.filter-reference/index.md b/src/contents/docs/expressions/03.filter-reference/index.md deleted file mode 100644 index bc730083061..00000000000 --- a/src/contents/docs/expressions/03.filter-reference/index.md +++ /dev/null @@ -1,594 +0,0 @@ ---- -title: Kestra Filter Reference – JSON, String, Date, and YAML Filters -description: Reference guide to the most important Kestra expression filters, grouped by data type and common use case. -sidebarTitle: Filter Reference -icon: /src/contents/docs/icons/expression.svg ---- - -Use filters when you need to transform a value with the pipe syntax: `{{ value | filterName(...) }}`. - -## Common filter categories - -- JSON and structured data -- numbers and collections -- strings -- dates and timestamps -- YAML formatting - -## JSON and structured data - -Use these filters when the value you already have is structured and you need to reshape it, serialize it, or extract one field from a larger payload. They are especially common when working with task outputs and API responses. - -### `toJson` - -Convert an object into JSON: - -```twig -{{ [1, 2, 3] | toJson }} -{{ true | toJson }} -{{ "foo" | toJson }} -``` - -### `toIon` - -Convert an object into Ion: - -```twig -{{ myObject | toIon }} -``` - -### `jq` - -Apply a JQ expression to a value. The result is always an array, so combine it with `first` when appropriate: - -```twig -{{ outputs | jq('.task1.value') | first }} -``` - -Examples: - -```twig -{{ [1, 2, 3] | jq('.') }} -{{ [1, 2, 3] | jq('.[0]') | first }} -``` - -Example flow using `jq` inside a `ForEach`: - -```yaml -id: jq_with_foreach -namespace: company.team - -tasks: - - id: generate - type: io.kestra.plugin.core.debug.Return - format: | - [ - {"name": "alpha", "value": 1}, - {"name": "bravo", "value": 2} - ] - - - id: foreach - type: io.kestra.plugin.core.flow.ForEach - values: "{{ fromJson(outputs.generate.value) }}" - tasks: - - id: log_filtered - type: io.kestra.plugin.core.log.Log - message: | - Name: {{ fromJson(taskrun.value).name }} - Doubled value: {{ fromJson(taskrun.value) | jq('.value * 2') | first }} -``` - -The practical rule with `jq` is that it is great for extracting or transforming a small part of a larger payload, but it is usually overkill when plain dot access already gets you the value you need. - -### Worked JSON payload example - -This larger example is useful when you need to mix accessors, math, collection helpers, and JSON-aware filters in one expression flow: - -```yaml -id: json_payload_example -namespace: company.team - -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 }}" - - "Score in languages: {{ inputs.payload.score.English + inputs.payload.score.French + inputs.payload.score.Spanish }}" - - "Total subjects: {{ inputs.payload.score | length }}" - - "Total score: {{ inputs.payload.score | values | jq('reduce .[] as $num (0; .+$num)') | first }}" - - "Complete address: {{ inputs.payload.address.city }}, {{ inputs.payload.address.country | upper }}" - - "Started college in: {{ inputs.payload.graduation_years | first }}" - - "Completed college in: {{ inputs.payload.graduation_years | last }}" -``` - -Use a pattern like this when the payload already arrives as JSON input and you want to keep the manipulation inside expressions instead of adding a preprocessing task. - -## Numbers and collections - -These filters are the everyday cleanup tools for expression values. Use them when you already have the right data but need to reformat it, count it, sort it, or coerce it into the type another task expects. - -### `abs` - -Returns the absolute value of a number: - -```twig -{{ -7 | abs }} -{# output: 7 #} -``` - -### `number` - -Parses a string into a numeric type. Supports `INT`, `FLOAT`, `LONG`, `DOUBLE`, `BIGDECIMAL`, and `BIGINTEGER`. When 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 #} -``` - -Use `BIGDECIMAL` or `BIGINTEGER` when values exceed standard long or double precision. - -### `className` - -Returns the Java class name of an object. Useful for debugging type inference when combined with `number`: - -```twig -{{ "12.3" | number | className }} -{# output: java.lang.Float #} -``` - -### `numberFormat` - -Formats a number using a Java `DecimalFormat` pattern: - -```twig -{{ 3.141592653 | numberFormat("#.##") }} -{# output: 3.14 #} -``` - -### `first` and `last` - -Returns the first or last element of a collection, or the first or last character of a string: - -```twig -{{ ['apple', 'banana', 'cherry'] | first }} -{# output: apple #} -{{ ['apple', 'banana', 'cherry'] | last }} -{# output: cherry #} -{{ 'Kestra' | first }} -{# output: K #} -{{ 'Kestra' | last }} -{# output: a #} -``` - -### `length` - -Returns the number of elements in a collection, or the number of characters in a string: - -```twig -{{ ['apple', 'banana'] | length }} -{# output: 2 #} -{{ 'Kestra' | length }} -{# output: 6 #} -``` - -### `join` - -Concatenates a collection into a single string with an optional delimiter: - -```twig -{{ ['apple', 'banana', 'cherry'] | join(', ') }} -{# output: apple, banana, cherry #} -``` - -### `split` - -Splits a string into a list using a delimiter. The delimiter is a regex, so escape special characters: - -```twig -{{ 'apple,banana,cherry' | split(',') }} -{# output: ['apple', 'banana', 'cherry'] #} -{{ 'a.b.c' | split('\\.') }} -``` - -The optional `limit` argument controls how many splits are performed: - -- **Positive**: limits the array size; the last entry contains the remaining content -- **Zero**: no limit; trailing empty strings are discarded -- **Negative**: no limit; trailing empty strings are included - -```twig -{{ 'apple,banana,cherry,grape' | split(',', 2) }} -{# output: ['apple', 'banana,cherry,grape'] #} -``` - -### `sort` and `rsort` - -Sort a collection in ascending or descending order: - -```twig -{{ [3, 1, 2] | sort }} -{# output: [1, 2, 3] #} -{{ [3, 1, 2] | rsort }} -{# output: [3, 2, 1] #} -``` - -### `reverse` - -Reverses the order of a collection: - -```twig -{{ [1, 2, 3] | reverse }} -{# output: [3, 2, 1] #} -``` - -### `chunk` - -Splits a collection into groups of a specified size: - -```twig -{{ [1, 2, 3, 4, 5] | chunk(2) }} -{# output: [[1, 2], [3, 4], [5]] #} -``` - -### `distinct` - -Returns only unique values from a collection: - -```twig -{{ [1, 2, 2, 3, 1] | distinct }} -{# output: [1, 2, 3] #} -``` - -### `slice` - -Extracts a portion of a collection or string using `fromIndex` (inclusive) and `toIndex` (exclusive): - -```twig -{{ ['apple', 'banana', 'cherry'] | slice(1, 2) }} -{# output: [banana] #} -{{ 'Kestra' | slice(1, 3) }} -{# output: es #} -``` - -### `merge` - -Merges two collections into one: - -```twig -{{ [1, 2] | merge([3, 4]) }} -{# output: [1, 2, 3, 4] #} -``` - -### `keys` and `values` - -Return the keys or values of a map: - -```twig -{{ {'foo': 'bar', 'baz': 'qux'} | keys }} -{# output: [foo, baz] #} -{{ {'foo': 'bar', 'baz': 'qux'} | values }} -{# output: [bar, qux] #} -``` - -## String filters - -String filters are where most small presentation fixes happen. They are usually the right tool for display formatting, filename shaping, templated messages, and API-compatible encodings. - -### Case and whitespace - -`lower`, `upper`, `title`, and `capitalize` normalize casing. `trim` removes leading and trailing whitespace. - -```twig -{{ "LOUD TEXT" | lower }} {# loud text #} -{{ "quiet text" | upper }} {# QUIET TEXT #} -{{ "article title" | title }} {# Article Title #} -{{ "hello world" | capitalize }} {# Hello world #} -{{ " padded " | trim }} {# padded #} -``` - -### `abbreviate` - -Truncates a string to a maximum length and appends an ellipsis. The length argument includes the ellipsis: - -```twig -{{ "this is a long sentence." | abbreviate(7) }} {# this... #} -``` - -Useful when you need to keep log messages or notification subjects within a character limit. - -### `replace` - -Substitutes one or more substrings using a map. Pass `regexp=true` to use regex patterns in the keys: - -```twig -{{ "I like %this% and %that%." | replace({'%this%': foo, '%that%': "bar"}) }} -``` - -### `substringBefore`, `substringAfter`, and their `Last` variants - -Extract the portion of a string before or after a delimiter. The `Last` variants match the final occurrence: - -```twig -{{ "a.b.c" | substringBefore(".") }} {# a #} -{{ "a.b.c" | substringAfter(".") }} {# b.c #} -{{ "a.b.c" | substringBeforeLast(".") }} {# a.b #} -{{ "a.b.c" | substringAfterLast(".") }} {# c #} -``` - -These are particularly useful for extracting file extensions, path segments, or identifier prefixes from task output values. - -### `slugify` - -Converts a string into a URL-safe slug: - -```twig -{{ "Hello World!" | slugify }} {# hello-world #} -``` - -### `default` - -Returns a fallback value when the expression is null or empty: - -```twig -{{ user.phoneNumber | default("No phone number") }} -``` - -### `startsWith` - -Returns `true` if the string begins with the given prefix: - -```twig -{{ "kestra://file.csv" | startsWith("kestra://") }} {# true #} -``` - -### Encoding and hashing - -`base64encode` and `base64decode` handle Base64 encoding. `urlencode` and `urldecode` percent-encode strings for use in URLs. `sha256` produces a hex-encoded SHA-256 hash. - -```twig -{{ "test" | base64encode }} -{# output: dGVzdA== #} -{{ "dGVzdA==" | base64decode }} -{# output: test #} -{{ "The string ü@foo-bar" | urlencode }} -{# output: The+string+%C3%BC%40foo-bar #} -{{ "The+string+%C3%BC%40foo-bar" | urldecode }} -{# output: The string ü@foo-bar #} -{{ "test" | sha256 }} -{# output: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 #} -``` - -### `string` - -Coerces any value to its string representation: - -```twig -{{ 42 | string }} -``` - -Use this when chaining filters that expect string input on a value that may arrive as a number or boolean. - -### `escapeChar` - -Escapes special characters in a string. The `type` argument controls which style of escaping is applied: `single`, `double`, or `shell`: - -```twig -{{ "Can't be here" | escapeChar('single') }} -{# output: Can\'t be here #} -``` - -### Worked string filter example - -This flow builds a sanitized filename and a display-safe summary from a raw input title: - -```yaml -id: string_filter_example -namespace: company.team - -inputs: - - id: title - type: STRING - defaults: " Quarterly Report: Q1 2025 (FINAL) " - -tasks: - - id: format_output - type: io.kestra.plugin.core.log.Log - message: - - "Trimmed: {{ inputs.title | trim }}" - - "Normalized: {{ inputs.title | trim | lower }}" - - "Slug (for filename): {{ inputs.title | trim | slugify }}" - - "Abbreviated (for subject line): {{ inputs.title | trim | abbreviate(30) }}" - - "Prefix check: {{ inputs.title | trim | startsWith('Quarterly') }}" - - "After colon: {{ inputs.title | trim | substringAfter(':') | trim }}" -``` - -## Temporal filters - -These are the most common filters in scheduled flows and integrations. Reach for them whenever a downstream system expects a specific date format or timestamp precision rather than Kestra's native datetime value. - -Use temporal filters to format dates or convert them to timestamps. - -### `date` - -```twig -{{ execution.startDate | date("yyyy-MM-dd") }} -``` - -You can also provide existing and target formats with named arguments: - -```twig -{{ stringDate | date(existingFormat="yyyy-MMMM-d", format="yyyy/MMMM/d") }} -``` - -When you are formatting an already parsed datetime, only `format` is usually needed. Use `existingFormat` when the source is still a plain string. - -### Time zones - -Specify a target time zone when downstream systems require a local representation rather than UTC: - -```twig -{{ now() | date("yyyy-MM-dd'T'HH:mm:ssX", timeZone="UTC") }} -``` - -Supported arguments include: - -- `format` -- `existingFormat` -- `timeZone` -- `locale` - -### `dateAdd` - -Adds or subtracts time from a date. Arguments: - -- `amount`: integer specifying how much to add or subtract -- `unit`: time unit such as `DAYS`, `HOURS`, `MONTHS`, or `YEARS` - -```twig -{{ now() | dateAdd(-1, 'DAYS') }} -``` - -### Timestamp helpers - -Convert a date to a Unix timestamp at a specific precision: - -- `timestamp` — seconds -- `timestampMilli` — milliseconds -- `timestampMicro` — microseconds -- `timestampNano` — nanoseconds - -:::alert{type="warning"} -`timestampMicro` previously returned a nanosecond-precision value due to a bug. If you are migrating an older flow, verify the precision your downstream system expects. -::: - -All timestamp filters accept the same arguments as the `date` filter: `existingFormat` and `timeZone`. - -```twig -{{ now() | timestamp(timeZone="Europe/Paris") }} -{{ now() | timestampMilli(timeZone="Asia/Kolkata") }} -``` - -Supported date formats include standard Java `DateTimeFormatter` patterns and shortcuts such as `iso`, `sql`, `iso_date_time`, and `iso_zoned_date_time`. - -### Temporal worked example - -```yaml -id: temporal_dates -namespace: company.team - -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 (milliseconds): {{ now() | timestampMilli(timeZone='Asia/Kolkata') }}" - - "Timezone (nanoseconds): {{ now() | timestampNano(timeZone='Asia/Kolkata') }}" -``` - -This kind of example is a good sanity check when you are validating timestamp precision before sending values to an external API. - -## YAML filters - -Use YAML filters when you are generating configuration or manifest-style text inside a task. They are less common in simple flows, but very useful in templated Kubernetes, Docker, or config-management patterns. - -### `yaml` - -Parse YAML into an object: - -```twig -{{ "foo: bar" | yaml }} -``` - -This is especially useful in templated tasks where the source data starts as text but later expressions need object-style access. - -#### Example: using `yaml` 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` and `nindent` - -Useful when generating templated YAML or embedding structured content: - -```twig -{{ labels | yaml | indent(4) }} -{{ variables.yaml_data | yaml | nindent(4) }} -``` - -#### 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) }} -``` - -Use `indent` when the first line is already in place and only following lines need alignment. Use `nindent` when you need to start a fresh indented block on the next line. - -## Choosing the right filter quickly - -| If you need to... | Use | -| --- | --- | -| Parse or transform JSON payloads | `toJson`, `jq`, `first` | -| Provide a fallback string or value | `default` | -| Format a date | `date` | -| Offset a date | `dateAdd` | -| Split or join text | `split`, `join` | -| Normalize casing | `lower`, `upper`, `title`, `capitalize` | -| Render YAML in a templated task | `yaml`, `indent`, `nindent` | diff --git a/src/contents/docs/expressions/04.function-reference/index.md b/src/contents/docs/expressions/04.function-reference/index.md deleted file mode 100644 index 6a1d86b0c78..00000000000 --- a/src/contents/docs/expressions/04.function-reference/index.md +++ /dev/null @@ -1,299 +0,0 @@ ---- -title: Kestra Function Reference – render, secret, read, and More -description: Reference guide to the main functions available in Kestra expressions, including rendering, secrets, file access, debugging, and utility helpers. -sidebarTitle: Function Reference -icon: /src/contents/docs/icons/expression.svg ---- - -Use functions when you need to generate or retrieve a value dynamically with syntax such as `{{ functionName(...) }}`. - -## Common function groups - -Functions are best thought of as helpers that either fetch something, compute something, or force evaluation behavior that plain variables and filters cannot provide on their own. - -### Rendering and debugging - -This group matters when expressions stop behaving the way you expect. `render()` and `printContext()` are often the quickest way to understand whether a value is missing, nested, or still just a string. - -- `render()` evaluates nested Pebble expressions -- `renderOnce()` renders a value only once -- `printContext()` outputs the full available context for debugging - -Examples: - -```twig -{{ render("{{ trigger.date ?? execution.startDate | date('yyyy-MM-dd') }}") }} -{{ printContext() }} -``` - -`renderOnce()` is the safer choice when you need one extra evaluation pass but do not want recursive expansion to keep walking nested Pebble content. - -### Secrets and file access - -These functions bridge expressions to external or stored data. Use them when the value is not already present in the execution context and must be resolved at runtime. - -- `secret()` reads a secret from Kestra's secret backend -- `credential()` reads a short-lived token from a managed EE credential -- `read()` reads the contents of a namespace file or internal-storage file -- `fileURI()` resolves a namespace file URI - -Examples: - -```twig -{{ secret('GITHUB_ACCESS_TOKEN') }} -{{ credential('my_oauth') }} -{{ read('subdir/file.txt') }} -{{ fileURI('my_file.txt') }} -``` - -`read()` accepts both namespace files and internal-storage URIs, which makes it useful after download or transformation tasks that write files as outputs. - -### Data parsing helpers - -These helpers are most useful when a task output is still a serialized string and you want to treat it like structured data in later expressions. - -- `fromJson()` -- `fromIon()` -- `yaml()` - -Examples: - -```twig -{{ fromJson('[1, 2, 3]')[0] }} -{{ fromIon(read(outputs.serialize.uri)).someField }} -{{ yaml('foo: [666, 1, 2]').foo[0] }} -``` - -### Execution and workflow helpers - -This group is more situational, but it becomes valuable in complex flows where you need to inspect sibling results, build links back into Kestra, or summarize failures. - -- `errorLogs()` for error summaries in alerts -- `currentEachOutput()` for simpler access to sibling outputs inside `ForEach` -- `iterationOutput()` to read outputs from previous or explicit `ForEach` iterations -- `tasksWithState()` to inspect tasks by state -- `appLink()` in Enterprise Edition to generate Kestra App URLs - -### Utility helpers - -- `now()` — returns the current datetime; accepts a `timeZone` argument: `now(timeZone="Europe/Paris")` -- `max(a, b, ...)` — returns the largest of its arguments -- `min(a, b, ...)` — returns the smallest of its arguments -- `range(start, end)` or `range(start, end, step)` — generates a list of integers up to and including `end`; the step defaults to 1 -- `uuid()` — generates a UUID in URL-safe base62 encoding; useful for unique identifiers in task inputs -- `randomInt(min, max)` — generates a random integer; the upper bound is **excluded** -- `randomPort()` — picks an available local port; useful in test or dev container flows -- `http(uri, ...)` — fetches a remote payload directly from an expression -- `fileSize(uri)` — returns the size in bytes of a file from internal storage -- `fileExists(uri)` — returns `true` if the file exists -- `fileEmpty(uri)` — returns `true` if the file has no content - -### Template inheritance helpers - -These are less common than runtime-oriented helpers, but they matter when you are using Pebble blocks and template inheritance directly. - -#### `block()` - -`block()` renders the contents of a named block multiple times. It is different from the Pebble `block` tag, which declares the block: - -```twig -{% block "post" %}content{% endblock %} - -{{ block("post") }} -``` - -#### `parent()` - -Use `parent()` inside an overriding block to include the original block content from the parent template: - -```twig -{% extends "parent.peb" %} - -{% block "content" %} -child content -{{ parent() }} -{% endblock %} -``` - -## Common function patterns - -The functions below are the ones most likely to shape a real flow. This section focuses on the practical cases where they change how you write expressions, not just what they do in isolation. - -### `render()` - -Use `render()` when a variable itself contains Pebble and must be evaluated: - -```twig -{{ render(namespace.github.token) }} -``` - -Without `render()`, namespace or flow variables that contain Pebble are treated as plain strings. - -### `secret()` - -Use `secret()` for sensitive values: - -```twig -{{ secret('API_KEY') }} -``` - -### `credential()` - -In Enterprise Edition, use `credential()` to inject a short-lived token from a managed credential: - -```twig -{{ credential('my_oauth') }} -``` - -Use [Execution Context Variables](../01.execution-context/index.md) for the setup model and a fuller HTTP example. `credential()` returns the token only, while the credential definition itself is managed in the Kestra UI. - -### `currentEachOutput()` - -Use it inside `ForEach` flows to avoid manual `taskrun.value` indexing: - -```twig -{{ currentEachOutput(outputs.make_data).values.data }} -``` - -### `iterationOutput()` - -Use `iterationOutput()` inside `ForEach` when the current iteration depends on work completed in an earlier loop index: - -```twig -{{ iterationOutput() }} -{{ iterationOutput('prefix_sum', taskrun.iteration - 1) }} -``` - -Defaulting behavior: - -- `iterationOutput()` uses the current task and the previous iteration -- `iterationOutput('taskId')` uses the provided task and the previous iteration -- `iterationOutput(null, 2)` uses the current task and iteration `2` -- `iterationOutput('taskId', null)` uses the provided task and the previous iteration - -Guard the first iteration explicitly, because there is no previous iteration to read when `taskrun.iteration == 0`. - -### `errorLogs()` - -Prints all error logs from the current execution: - -```twig -{{ errorLogs() }} -``` - -It is most useful in `errors` blocks, where you need a compact summary of what failed without manually traversing task state objects. - -### `fromIon()` - -Use `fromIon()` when a previous task or serializer produces Ion rather than JSON: - -```twig -{{ fromIon(read(outputs.serialize.uri)).someField }} -``` - -### `read()` - -`read()` is the simplest way to turn a file URI back into inline content for a later expression: - -```twig -{{ read(outputs.someTask.uri) }} -``` - -### `renderOnce()` - -Equivalent to `render(expression, recursive=false)`: - -```twig -{{ renderOnce(namespace.github.token) }} -``` - -### Numeric and generation helpers - -```twig -{{ max(5, 10, 15) }} -{# output: 15 #} -{{ min(5, 10, 15) }} -{# output: 5 #} -{{ now() }} -{{ now(timeZone="Europe/Paris") }} -{{ range(0, 3) }} -{# output: [0, 1, 2, 3] #} -{{ range(0, 6, 2) }} -{# output: [0, 2, 4, 6] #} -{{ uuid() }} -{{ randomInt(1, 10) }} -{# generates a random integer from 1 to 9 (10 is excluded) #} -``` - -### File and runtime helpers - -These helpers are usually used in operational flows rather than day-to-day templating: - -```twig -{{ randomPort() }} -{{ fileSize(outputs.download.uri) }} -{{ fileExists(outputs.download.uri) }} -{{ fileEmpty(outputs.download.uri) }} -{{ tasksWithState('failed') }} -``` - -### `http()` - -`http()` lets an expression fetch a remote payload directly: - -```twig -{{ http(uri = 'https://dummyjson.com/products/categories') | jq('.[].slug') }} -``` - -Use it sparingly. It is convenient for dynamic dropdowns and lightweight lookups, but task-level HTTP calls are usually easier to observe and retry. - -### `isIn()` - -Use `isIn()` for conditions where one value must be tested against a short allowlist: - -```twig -{{ execution.state isIn ['SUCCESS', 'KILLED', 'CANCELLED'] }} -``` - -That reads more clearly than chaining multiple equality checks in `runIf`, SLAs, or alert conditions. - -### `appLink()` - -Enterprise Edition's `appLink()` builds links back to Kestra Apps: - -```twig -{{ appLink(appId='com.example.my-app') }} -{{ appLink(baseUrl=true) }} -``` - -Use it in notifications when you want recipients to jump directly into the related app rather than the generic flow UI. - -## Worked example - -This flow uses several runtime functions together: `now()` for a timestamp, `uuid()` for a unique run identifier, `secret()` for a credential, and `render()` to evaluate a namespace variable containing Pebble: - -```yaml -id: function_reference_example -namespace: company.team - -tasks: - - id: log_context - type: io.kestra.plugin.core.log.Log - message: - - "Run ID: {{ uuid() }}" - - "Started at: {{ now() | date('yyyy-MM-dd HH:mm:ss') }}" - - "API key: {{ secret('MY_API_KEY') }}" - - "Config value: {{ render(namespace.my_config) }}" - - - id: check_file - type: io.kestra.plugin.core.log.Log - message: | - File exists: {{ fileExists(outputs.log_context.uri) }} - File size: {{ fileSize(outputs.log_context.uri) }} bytes -``` - -## Related pages - -- Need variables like `inputs`, `outputs`, or `namespace`: [Execution Context Variables](../01.execution-context/index.md) -- Need filters like `date`, `default`, or `jq`: [Filter Reference](../03.filter-reference/index.md) -- Need Pebble syntax and nested rendering patterns: [Pebble Syntax](../02.pebble-syntax/index.md) diff --git a/src/contents/docs/expressions/05.operators-tags-tests/index.md b/src/contents/docs/expressions/05.operators-tags-tests/index.md deleted file mode 100644 index 222e8decb28..00000000000 --- a/src/contents/docs/expressions/05.operators-tags-tests/index.md +++ /dev/null @@ -1,346 +0,0 @@ ---- -title: Kestra Expressions Operators, Tags, and Tests -description: Learn the operators, tags, and tests used in Kestra expressions for comparisons, control flow, loops, fallbacks, and template logic. -sidebarTitle: Operators, Tags, and Tests -icon: /src/contents/docs/icons/expression.svg ---- - -Use this page when you need the control-flow side of Pebble rather than data transformation helpers. - -## Operators - -### Comparisons - -Supported comparison operators: - -- `==` -- `!=` -- `<` -- `>` -- `<=` -- `>=` - -### Logic and boolean checks - -Use: - -- `and` -- `or` -- `not` -- `is` -- `contains` - -Use parentheses to group expressions and make precedence explicit: - -```twig -{% if 2 is even and 3 is odd %} - ... -{% endif %} - -{% if (3 is not even) and (2 is odd or 3 is even) %} - ... -{% endif %} -``` - -### `contains` - -Checks whether an item exists within a list, string, map, or array: - -```twig -{% if ["apple", "pear", "banana"] contains "apple" %} - ... -{% endif %} -``` - -For maps, `contains` checks for a matching key: - -```twig -{% if {"apple": "red", "banana": "yellow"} contains "banana" %} - ... -{% endif %} -``` - -To check for multiple items at once, pass a list on the right-hand side: - -```twig -{% if ["apple", "pear", "banana", "peach"] contains ["apple", "peach"] %} - ... -{% endif %} -``` - -`contains` also works inline in output expressions: - -```twig -{{ inputs.mainString contains inputs.subString }} -``` - -### Math and concatenation - -Use: - -- `+`, `-`, `*`, `/`, `%` -- `~` for string concatenation - -Example: - -```twig -{{ "apple" ~ "pear" ~ "banana" }} -{{ 2 + 2 / (10 % 3) * (8 - 1) }} -``` - -### Fallbacks and conditionals - -Use: - -- `??` for null-coalescing: returns the first non-null value -- `???` for undefined-coalescing: returns the right-hand side only when the left is undefined (not just null) -- `? :` for ternary expressions - -Examples: - -```twig -{{ foo ?? bar ?? "default" }} {# first non-null value #} -{{ foo ??? "default" }} {# only if foo is undefined #} -{{ foo == null ? bar : baz }} -{{ foo ?? bar ?? raise }} {# raises an exception if all are undefined #} -``` - -For detailed null vs undefined behavior, see the [Handling null and undefined values](../../15.how-to-guides/null-values/index.md) guide. - -### Operator precedence - -Pebble operators are evaluated in this order: - -1. `.` -2. `|` -3. `%`, `/`, `*` -4. `-`, `+` -5. `==`, `!=`, `>`, `<`, `>=`, `<=` -6. `is`, `is not` -7. `and` -8. `or` - -## Tags - -Pebble tags are enclosed in `{% %}` and control template flow. - -### `set` - -Defines a variable in the template context: - -```twig -{% set header = "Welcome Page" %} -{{ header }} -{# output: Welcome Page #} -``` - -### `if` - -Evaluates conditional logic. Use `elseif` and `else` for multiple branches: - -```twig -{% if users is empty %} - No users available. -{% elseif users.length == 1 %} - One user found. -{% else %} - Multiple users found. -{% endif %} -``` - -### `for` - -Iterates over arrays, maps, or any `java.lang.Iterable`. - -**Iterating over a list:** - -```twig -{% for user in users %} - {{ user.name }} lives in {{ user.city }}. -{% else %} - No users found. -{% endfor %} -``` - -The `else` block runs when the collection is empty. - -**Iterating over a map:** - -```twig -{% for entry in map %} - {{ entry.key }}: {{ entry.value }} -{% endfor %} -``` - -**Loop special variables:** - -Inside any `for` loop, Pebble provides a `loop` object with these properties: - -| Variable | Description | -| --- | --- | -| `loop.index` | Zero-based index of the current iteration | -| `loop.length` | Total number of items in the iterable | -| `loop.first` | `true` on the first iteration | -| `loop.last` | `true` on the last iteration | -| `loop.revindex` | Number of iterations remaining | - -Example: - -```twig -{% for user in users %} - {{ loop.index }}: {{ user.name }}{% if loop.last %} (last){% endif %} -{% endfor %} -``` - -### `filter` - -Applies a filter to a block of content. Filters can be chained: - -```twig -{% filter upper %} - hello -{% endfilter %} -{# output: HELLO #} - -{% filter lower | title %} - hello world -{% endfilter %} -{# output: Hello World #} -``` - -### `raw` - -Prevents Pebble from parsing its content — useful when you need to output literal `{{ }}` syntax: - -```twig -{% raw %}{{ user.name }}{% endraw %} -{# output: {{ user.name }} #} -``` - -### `macro` - -Defines a reusable template snippet. Macros only have access to their own arguments by default: - -```twig -{% macro input(type="text", name, value="") %} - type: "{{ type }}", name: "{{ name }}", value: "{{ value }}" -{% endmacro %} - -{{ input(name="country") }} -{# output: type: "text", name: "country", value: "" #} -``` - -To access variables from the outer template context, pass `_context` explicitly: - -```twig -{% set foo = "bar" %} - -{% macro display(_context) %} - {{ _context.foo }} -{% endmacro %} - -{{ display(_context) }} -{# output: bar #} -``` - -### `block` - -Defines a named, reusable template block. Use the `block()` function to render the block elsewhere: - -```twig -{% block "header" %} - Introduction -{% endblock %} - -{{ block("header") }} -``` - -## Tests - -Tests are used with `is` and `is not` to perform type and value checks. - -### `defined` - -Checks whether a variable exists in the context (regardless of its value): - -```twig -{% if missing is not defined %} - Variable is not defined. -{% endif %} -``` - -### `empty` - -Returns `true` when a variable is null, an empty string, an empty collection, or an empty map: - -```twig -{% if user.email is empty %} - No email on record. -{% endif %} -``` - -### `null` - -Checks whether a variable is null: - -```twig -{% if user.email is null %} - ... -{% endif %} - -{% if name is not null %} - ... -{% endif %} -``` - -### `even` and `odd` - -Check whether an integer is even or odd: - -```twig -{% if 2 is even %} - ... -{% endif %} - -{% if 3 is odd %} - ... -{% endif %} -``` - -### `iterable` - -Returns `true` when a variable implements `java.lang.Iterable`. Use this to guard a `for` loop when the collection may not always be present: - -```twig -{% if users is iterable %} - {% for user in users %} - {{ user.name }} - {% endfor %} -{% endif %} -``` - -### `json` - -Returns `true` when a variable is a valid JSON string: - -```twig -{% if '{"test": 1}' is json %} - ... -{% endif %} -``` - -### `map` - -Returns `true` when a variable is a map: - -```twig -{% if {"apple": "red", "banana": "yellow"} is map %} - ... -{% endif %} -``` - -## When to use this page - -- Need expression-writing basics: [Pebble Syntax](../02.pebble-syntax/index.md) -- Need data transformations: [Filter Reference](../03.filter-reference/index.md) -- Need runtime helpers: [Function Reference](../04.function-reference/index.md) diff --git a/src/contents/docs/expressions/index.mdx b/src/contents/docs/expressions/index.mdx index 28881766562..ddc59e0d3bc 100644 --- a/src/contents/docs/expressions/index.mdx +++ b/src/contents/docs/expressions/index.mdx @@ -5,32 +5,1719 @@ sidebarTitle: Expressions icon: /src/contents/docs/icons/expression.svg --- -import ChildCard from "~/components/docs/ChildCard.astro" - Use expressions to dynamically set values in flows using `{{ ... }}` syntax backed by the Pebble templating engine.
-## Find the right expressions doc - -- Start with [Execution Context Variables](./01.execution-context/index.md) if you need to know what values are available in flows. -- Go to [Pebble Syntax](./02.pebble-syntax/index.md) if you need help writing expressions. -- Use [Filter Reference](./03.filter-reference/index.md) when you know you need a filter but are not sure which category it belongs to. -- Use [Function Reference](./04.function-reference/index.md) for runtime helpers such as `render()`, `secret()`, `read()`, and `printContext()`. -- Use [Operators, Tags, and Tests](./05.operators-tags-tests/index.md) for control flow and boolean logic. - ## Common tasks | If you need to... | Start here | | --- | --- | -| Access `inputs`, `outputs`, `vars`, `trigger`, or `namespace` values | [Execution Context Variables](./01.execution-context/index.md) | -| Format dates, parse JSON, or transform strings | [Filter Reference](./03.filter-reference/index.md) | -| Render nested expressions or inspect the full context | [Function Reference](./04.function-reference/index.md) | -| Write loops, conditions, fallbacks, and comparisons | [Pebble Syntax](./02.pebble-syntax/index.md) and [Operators, Tags, and Tests](./05.operators-tags-tests/index.md) | -| Build or debug a multiline or nested expression | [Pebble Syntax](./02.pebble-syntax/index.md#multiline-json-bodies) and [Function Reference](./04.function-reference/index.md#render) | +| Access `inputs`, `outputs`, `vars`, `trigger`, or `namespace` values | [Execution Context Variables](#execution-context-variables) | +| Access secrets or credentials at runtime | [Secrets and file access](#secrets-and-file-access) | +| Format dates, parse JSON, or transform strings | [Filter Reference](#filter-reference) | +| Render nested expressions or inspect the full context | [Function Reference](#function-reference) | +| Write loops, conditions, fallbacks, and comparisons | [Pebble Syntax](#pebble-syntax) and [Operators, Tags, and Tests](#operators-tags-and-tests) | +| Build or debug a multiline or nested expression | [Multiline JSON bodies](#multiline-json-bodies) and [`render()`](#render) | + +## Execution Context Variables + +Use this section to find out what data is available inside `{{ ... }}` at runtime — including flow metadata, inputs, outputs, trigger values, secrets, and namespace variables. + +### Understand the execution context + +Kestra expressions combine the [Pebble templating engine](../../06.concepts/06.pebble/index.md) with the execution context to dynamically render flow properties. + +The execution context usually includes: + +- `flow` +- `execution` +- `inputs` +- `outputs` +- `labels` +- `tasks` +- `trigger` when the flow was started by a trigger +- `vars` when the flow defines variables +- `namespace` in Enterprise Edition when namespace variables are configured +- `envs` for environment variables +- `globals` for global configuration values + +:::alert{type="info"} +To inspect the full runtime context, use `{{ printContext() }}` in the Debug Expression console. +::: + +The Debug Expression console is available in the Kestra UI under **Executions → Logs → Debug Expression**. Enter any expression and evaluate it against the live execution context without modifying the flow. + +
+ +
+ +### Default execution context variables + +| Parameter | Description | +| --- | --- | +| `{{ flow.id }}` | Identifier of the flow | +| `{{ flow.namespace }}` | Namespace of the flow | +| `{{ flow.tenantId }}` | Tenant identifier in Enterprise Edition | +| `{{ flow.revision }}` | Flow revision number | +| `{{ execution.id }}` | Unique execution identifier | +| `{{ execution.startDate }}` | Start date of the execution | +| `{{ execution.state }}` | Current execution state | +| `{{ execution.originalId }}` | Original execution ID preserved across replays | +| `{{ task.id }}` | Current task identifier | +| `{{ task.type }}` | Fully qualified class name of the current task | +| `{{ taskrun.id }}` | Current task run identifier | +| `{{ taskrun.startDate }}` | Start date of the current task run | +| `{{ taskrun.attemptsCount }}` | Retry and restart attempt count | +| `{{ taskrun.parentId }}` | Parent task run identifier for nested tasks | +| `{{ taskrun.value }}` | Current loop or flowable value | +| `{{ parent.taskrun.value }}` | Value of the nearest parent task run | +| `{{ parent.outputs }}` | Outputs of the nearest parent task run | +| `{{ parents }}` | List of parent task runs | +| `{{ labels }}` | Execution labels accessible by key | + +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") }} +``` + +### Trigger variables + +When the execution is started by a `Schedule` trigger: + +| Parameter | Description | +| --- | --- | +| `{{ trigger.date }}` | Date of the current schedule | +| `{{ trigger.next }}` | Date of the next schedule | +| `{{ trigger.previous }}` | Date of the previous schedule | + +When the execution is started by a `Flow` trigger: + +| Parameter | Description | +| --- | --- | +| `{{ trigger.executionId }}` | ID of the triggering execution | +| `{{ trigger.namespace }}` | Namespace of the triggering flow | +| `{{ trigger.flowId }}` | ID of the triggering flow | +| `{{ trigger.flowRevision }}` | Revision of the triggering flow | + +### Environment and global variables + +Kestra provides access to environment variables prefixed with `ENV_` by default, unless configured otherwise in the [runtime and storage configuration](../../configuration/02.runtime-and-storage/index.md). + +- reference `ENV_FOO` as `{{ envs.foo }}` +- reference the configured environment name as `{{ kestra.environment }}` +- reference the configured Kestra URL as `{{ kestra.url }}` +- reference global variables from configuration as `{{ globals.foo }}` + +### Flow variables and inputs + +Use flow-level variables with `vars.*`: + +```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 }}" +``` + +Use inputs with `inputs.*`: + +```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, credentials, namespace variables, and outputs + +Use `secret()` to inject secret values at runtime: + +```yaml +tasks: + - id: myTask + type: io.kestra.plugin.core.debug.Return + format: "{{ secret('MY_SECRET') }}" +``` + +Use `credential()` in Enterprise Edition to inject a short-lived token from a managed [Credential](../../07.enterprise/03.auth/credentials/index.md): + +```yaml +tasks: + - id: request + type: io.kestra.plugin.core.http.Request + method: GET + uri: https://api.example.com/v1/ping + auth: + type: BEARER + token: "{{ credential('my_oauth') }}" +``` + +`credential()` returns the short-lived token only. The credential itself is managed in the Kestra UI. + +Use namespace variables in Enterprise Edition with `namespace.*`. To set them up: + +1. Open the Kestra UI and navigate to **Namespaces**. +2. Select the namespace where the flow runs. +3. Open the **Variables** tab. +4. Add a key-value pair such as `github.token` with the desired value. + +Reference namespace variables in expressions using dot notation: + +```yaml +format: "{{ namespace.github.token }}" +``` + +If a namespace variable itself contains Pebble, evaluate it with `render()`: + +```yaml +format: "{{ render(namespace.github.token) }}" +``` + +Use outputs with `outputs.taskId.attribute`: + +```yaml +message: | + First: {{ outputs.first.value }} + Second: {{ outputs['second-task'].value }} +``` + +:::alert{type="info"} +If a task ID or output key contains a hyphen, use bracket notation such as `outputs['second-task']`. To avoid that, prefer `camelCase` or `snake_case`. +::: + +## Pebble Syntax + +Use this section when you need help writing expressions — delimiters, attribute access, nested rendering, control flow, and fallback patterns. + +### Pebble basics + +Pebble templates use two primary delimiters: + +- `{{ ... }}` to output the result of an expression +- `{% ... %}` to control template flow with tags such as `if`, `for`, or `set` + +Examples: + +```twig +{{ flow.id }} +{% if inputs.region == "eu" %}Europe{% endif %} +``` + +To escape Pebble syntax literally, use the `raw` tag described in [Operators, Tags, and Tests](#raw). + +### Accessing values + +Use dot notation for standard property access: + +```twig +{{ foo.bar }} +``` + +Use bracket notation for special characters or indexed access: + +```twig +{{ foo['foo-bar'] }} +{{ items[0] }} +``` + +:::alert{type="warning"} +If a task ID, output key, or attribute contains a hyphen, use bracket notation. To avoid that, prefer `camelCase` or `snake_case`. +::: + +### Parsing nested expressions + +Kestra renders expressions once by default. If a variable contains Pebble that should be evaluated later, use `render()`: + +```yaml +variables: + trigger_or_yesterday: "{{ trigger.date ?? (execution.startDate | dateAdd(-1, 'DAYS')) }}" + input_or_yesterday: "{{ inputs.mydate ?? (execution.startDate | dateAdd(-1, 'DAYS')) }}" + +tasks: + - id: yesterday + type: io.kestra.plugin.core.log.Log + message: "{{ render(vars.trigger_or_yesterday) }}" + + - id: input_or_yesterday + type: io.kestra.plugin.core.log.Log + message: "{{ render(vars.input_or_yesterday) }}" +``` + +This pattern is especially useful with namespace variables, composed flow variables, and fallback logic based on trigger context. + +#### Multiline JSON bodies + +When an HTTP request body contains multiline user input, avoid partial string interpolation. Instead, build the whole payload as a single Pebble expression so JSON escaping happens correctly. + +```yaml +id: multiline_input_passed_to_json_body +namespace: company.team + +inputs: + - id: title + type: STRING + defaults: This is my title + - id: message + type: STRING + defaults: |- + This is my long + multiline message. + - id: priority + type: INT + defaults: 5 + +tasks: + - id: hello + type: io.kestra.plugin.core.http.Request + uri: https://kestra.io/api/mock + method: POST + body: | + {{ { + "title": inputs.title, + "message": inputs.message, + "priority": inputs.priority + } | toJson }} +``` + +### Common syntax patterns + +#### Comments + +Use Pebble comments with `{# ... #}`: + +```twig +{# This is a comment #} +{{ "Visible content" }} +``` + +In YAML, continue to use `#` for comments outside the expression itself. + +#### Literals and collections + +Pebble supports: + +- strings: `"Hello World"` +- numbers such as `100 + 10l * 2.5` +- booleans: `true`, `false` +- null: `null` +- lists: `["apple", "banana"]` +- maps: `{"apple":"red", "banana":"yellow"}` + +#### Named arguments + +Filters, functions, and macros can accept named arguments: + +```twig +{{ stringDate | date(existingFormat="yyyy-MMMM-d", format="yyyy/MMMM/d") }} +``` + +### Control flow and fallbacks + +Common patterns: + +- `if` and `elseif` for branching +- `for` for iteration +- `??` for fallback values +- `? :` for ternary expressions + +Examples: + +```twig +{{ inputs.mydate ?? (execution.startDate | dateAdd(-1, 'DAYS')) }} +``` + +```twig +{% for article in articles %} + {{ article.title }} +{% else %} + No articles available. +{% endfor %} +``` + +Inside a `for` loop, Pebble provides a `loop` object with properties such as `loop.index`, `loop.first`, `loop.last`, and `loop.length`. For the full table and examples, see [Operators, Tags, and Tests](#for). + +```twig +{% if category == "news" %} + {{ news }} +{% elseif category == "sports" %} + {{ sports }} +{% else %} + Select a category +{% endif %} +``` + +## Filter Reference + +Use filters when you need to transform a value with the pipe syntax: `{{ value | filterName(...) }}`. + +### Common filter categories + +- JSON and structured data +- numbers and collections +- strings +- dates and timestamps +- YAML formatting + +### JSON and structured data + +Use these filters when the value you already have is structured and you need to reshape it, serialize it, or extract one field from a larger payload. They are especially common when working with task outputs and API responses. + +#### `toJson` + +Convert an object into JSON: + +```twig +{{ [1, 2, 3] | toJson }} +{{ true | toJson }} +{{ "foo" | toJson }} +``` + +#### `toIon` + +Convert an object into Ion: + +```twig +{{ myObject | toIon }} +``` + +#### `jq` + +Apply a JQ expression to a value. The result is always an array, so combine it with `first` when appropriate: + +```twig +{{ outputs | jq('.task1.value') | first }} +``` + +Examples: + +```twig +{{ [1, 2, 3] | jq('.') }} +{{ [1, 2, 3] | jq('.[0]') | first }} +``` + +Example flow using `jq` inside a `ForEach`: + +```yaml +id: jq_with_foreach +namespace: company.team + +tasks: + - id: generate + type: io.kestra.plugin.core.debug.Return + format: | + [ + {"name": "alpha", "value": 1}, + {"name": "bravo", "value": 2} + ] + + - id: foreach + type: io.kestra.plugin.core.flow.ForEach + values: "{{ fromJson(outputs.generate.value) }}" + tasks: + - id: log_filtered + type: io.kestra.plugin.core.log.Log + message: | + Name: {{ fromJson(taskrun.value).name }} + Doubled value: {{ fromJson(taskrun.value) | jq('.value * 2') | first }} +``` + +The practical rule with `jq` is that it is great for extracting or transforming a small part of a larger payload, but it is usually overkill when plain dot access already gets you the value you need. + +#### Worked JSON payload example + +This larger example is useful when you need to mix accessors, math, collection helpers, and JSON-aware filters in one expression flow: + +```yaml +id: json_payload_example +namespace: company.team + +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 }}" + - "Score in languages: {{ inputs.payload.score.English + inputs.payload.score.French + inputs.payload.score.Spanish }}" + - "Total subjects: {{ inputs.payload.score | length }}" + - "Total score: {{ inputs.payload.score | values | jq('reduce .[] as $num (0; .+$num)') | first }}" + - "Complete address: {{ inputs.payload.address.city }}, {{ inputs.payload.address.country | upper }}" + - "Started college in: {{ inputs.payload.graduation_years | first }}" + - "Completed college in: {{ inputs.payload.graduation_years | last }}" +``` + +Use a pattern like this when the payload already arrives as JSON input and you want to keep the manipulation inside expressions instead of adding a preprocessing task. + +### Numbers and collections + +These filters are the everyday cleanup tools for expression values. Use them when you already have the right data but need to reformat it, count it, sort it, or coerce it into the type another task expects. + +#### `abs` + +Returns the absolute value of a number: + +```twig +{{ -7 | abs }} +{# output: 7 #} +``` + +#### `number` + +Parses a string into a numeric type. Supports `INT`, `FLOAT`, `LONG`, `DOUBLE`, `BIGDECIMAL`, and `BIGINTEGER`. When 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 #} +``` + +Use `BIGDECIMAL` or `BIGINTEGER` when values exceed standard long or double precision. + +#### `className` + +Returns the Java class name of an object. Useful for debugging type inference when combined with `number`: + +```twig +{{ "12.3" | number | className }} +{# output: java.lang.Float #} +``` + +#### `numberFormat` + +Formats a number using a Java `DecimalFormat` pattern: + +```twig +{{ 3.141592653 | numberFormat("#.##") }} +{# output: 3.14 #} +``` + +#### `first` and `last` + +Returns the first or last element of a collection, or the first or last character of a string: + +```twig +{{ ['apple', 'banana', 'cherry'] | first }} +{# output: apple #} +{{ ['apple', 'banana', 'cherry'] | last }} +{# output: cherry #} +{{ 'Kestra' | first }} +{# output: K #} +{{ 'Kestra' | last }} +{# output: a #} +``` + +#### `length` + +Returns the number of elements in a collection, or the number of characters in a string: + +```twig +{{ ['apple', 'banana'] | length }} +{# output: 2 #} +{{ 'Kestra' | length }} +{# output: 6 #} +``` + +#### `join` + +Concatenates a collection into a single string with an optional delimiter: + +```twig +{{ ['apple', 'banana', 'cherry'] | join(', ') }} +{# output: apple, banana, cherry #} +``` + +#### `split` + +Splits a string into a list using a delimiter. The delimiter is a regex, so escape special characters: + +```twig +{{ 'apple,banana,cherry' | split(',') }} +{# output: ['apple', 'banana', 'cherry'] #} +{{ 'a.b.c' | split('\\.') }} +``` + +The optional `limit` argument controls how many splits are performed: + +- **Positive**: limits the array size; the last entry contains the remaining content +- **Zero**: no limit; trailing empty strings are discarded +- **Negative**: no limit; trailing empty strings are included + +```twig +{{ 'apple,banana,cherry,grape' | split(',', 2) }} +{# output: ['apple', 'banana,cherry,grape'] #} +``` + +#### `sort` and `rsort` + +Sort a collection in ascending or descending order: + +```twig +{{ [3, 1, 2] | sort }} +{# output: [1, 2, 3] #} +{{ [3, 1, 2] | rsort }} +{# output: [3, 2, 1] #} +``` + +#### `reverse` + +Reverses the order of a collection: + +```twig +{{ [1, 2, 3] | reverse }} +{# output: [3, 2, 1] #} +``` + +#### `chunk` + +Splits a collection into groups of a specified size: + +```twig +{{ [1, 2, 3, 4, 5] | chunk(2) }} +{# output: [[1, 2], [3, 4], [5]] #} +``` + +#### `distinct` + +Returns only unique values from a collection: + +```twig +{{ [1, 2, 2, 3, 1] | distinct }} +{# output: [1, 2, 3] #} +``` + +#### `slice` + +Extracts a portion of a collection or string using `fromIndex` (inclusive) and `toIndex` (exclusive): + +```twig +{{ ['apple', 'banana', 'cherry'] | slice(1, 2) }} +{# output: [banana] #} +{{ 'Kestra' | slice(1, 3) }} +{# output: es #} +``` + +#### `merge` + +Merges two collections into one: + +```twig +{{ [1, 2] | merge([3, 4]) }} +{# output: [1, 2, 3, 4] #} +``` + +#### `flatten` + +Removes one level of nesting from a collection: + +```twig +{{ [[1, 2], [3, 4], [5]] | flatten }} +{# output: [1, 2, 3, 4, 5] #} +``` + +#### `keys` and `values` + +Return the keys or values of a map: + +```twig +{{ {'foo': 'bar', 'baz': 'qux'} | keys }} +{# output: [foo, baz] #} +{{ {'foo': 'bar', 'baz': 'qux'} | values }} +{# output: [bar, qux] #} +``` + +### String filters + +String filters are where most small presentation fixes happen. They are usually the right tool for display formatting, filename shaping, templated messages, and API-compatible encodings. + +#### Case and whitespace + +`lower`, `upper`, `title`, and `capitalize` normalize casing. `trim` removes leading and trailing whitespace. + +```twig +{{ "LOUD TEXT" | lower }} {# loud text #} +{{ "quiet text" | upper }} {# QUIET TEXT #} +{{ "article title" | title }} {# Article Title #} +{{ "hello world" | capitalize }} {# Hello world #} +{{ " padded " | trim }} {# padded #} +``` + +#### `abbreviate` + +Truncates a string to a maximum length and appends an ellipsis. The length argument includes the ellipsis: + +```twig +{{ "this is a long sentence." | abbreviate(7) }} {# this... #} +``` + +Useful when you need to keep log messages or notification subjects within a character limit. + +#### `replace` + +Substitutes one or more substrings using a map. Pass `regexp=true` to use regex patterns in the keys: + +```twig +{{ "I like %this% and %that%." | replace({'%this%': foo, '%that%': "bar"}) }} +``` + +#### `substringBefore`, `substringAfter`, and their `Last` variants + +Extract the portion of a string before or after a delimiter. The `Last` variants match the final occurrence: + +```twig +{{ "a.b.c" | substringBefore(".") }} {# a #} +{{ "a.b.c" | substringAfter(".") }} {# b.c #} +{{ "a.b.c" | substringBeforeLast(".") }} {# a.b #} +{{ "a.b.c" | substringAfterLast(".") }} {# c #} +``` + +These are particularly useful for extracting file extensions, path segments, or identifier prefixes from task output values. + +#### `slugify` + +Converts a string into a URL-safe slug: + +```twig +{{ "Hello World!" | slugify }} {# hello-world #} +``` + +#### `default` + +Returns a fallback value when the expression is null or empty: + +```twig +{{ user.phoneNumber | default("No phone number") }} +``` + +#### `startsWith` + +Returns `true` if the string begins with the given prefix: + +```twig +{{ "kestra://file.csv" | startsWith("kestra://") }} {# true #} +``` + +#### `endsWith` + +Returns `true` if the string ends with the given suffix: + +```twig +{{ "report.csv" | endsWith(".csv") }} {# true #} +``` + +#### Encoding and hashing + +`base64encode` and `base64decode` handle Base64 encoding. `urlencode` and `urldecode` percent-encode strings for use in URLs. `sha1`, `sha512`, and `md5` produce hex-encoded hashes of the corresponding algorithms. + +```twig +{{ "test" | base64encode }} +{# output: dGVzdA== #} +{{ "dGVzdA==" | base64decode }} +{# output: test #} +{{ "The string ü@foo-bar" | urlencode }} +{# output: The+string+%C3%BC%40foo-bar #} +{{ "The+string+%C3%BC%40foo-bar" | urldecode }} +{# output: The string ü@foo-bar #} +{{ "test" | sha1 }} +{{ "test" | sha512 }} +{{ "test" | md5 }} +``` + +#### `string` + +Coerces any value to its string representation: + +```twig +{{ 42 | string }} +``` + +Use this when chaining filters that expect string input on a value that may arrive as a number or boolean. + +#### `escapeChar` + +Escapes special characters in a string. The `type` argument controls which style of escaping is applied: `single`, `double`, or `shell`: + +```twig +{{ "Can't be here" | escapeChar('single') }} +{# output: Can\'t be here #} +``` + +#### Regex filters + +`regexMatch(regex)` returns `true` if the input contains a substring matching the pattern. `regexReplace(regex, replacement)` replaces all matching substrings. `regexExtract(regex, group)` returns the first match or a specific capture group (`group` defaults to `0`; returns `null` if no match): + +```twig +{{ "hello world" | regexMatch("w[a-z]+") }} +{# output: true #} +{{ "2024-01-15" | regexReplace("(\\d{4})-(\\d{2})-(\\d{2})", "$3/$2/$1") }} +{# output: 15/01/2024 #} +{{ "order-12345-done" | regexExtract("\\d+") }} +{# output: 12345 #} +{{ "2024-01-15" | regexExtract("(\\d{4})-(\\d{2})-(\\d{2})", 1) }} +{# output: 2024 #} +``` + +#### Worked string filter example + +This flow builds a sanitized filename and a display-safe summary from a raw input title: + +```yaml +id: string_filter_example +namespace: company.team + +inputs: + - id: title + type: STRING + defaults: " Quarterly Report: Q1 2025 (FINAL) " + +tasks: + - id: format_output + type: io.kestra.plugin.core.log.Log + message: + - "Trimmed: {{ inputs.title | trim }}" + - "Normalized: {{ inputs.title | trim | lower }}" + - "Slug (for filename): {{ inputs.title | trim | slugify }}" + - "Abbreviated (for subject line): {{ inputs.title | trim | abbreviate(30) }}" + - "Prefix check: {{ inputs.title | trim | startsWith('Quarterly') }}" + - "After colon: {{ inputs.title | trim | substringAfter(':') | trim }}" +``` + +### Temporal filters + +These are the most common filters in scheduled flows and integrations. Reach for them whenever a downstream system expects a specific date format or timestamp precision rather than Kestra's native datetime value. + +Use temporal filters to format dates or convert them to timestamps. + +#### `date` + +```twig +{{ execution.startDate | date("yyyy-MM-dd") }} +``` + +You can also provide existing and target formats with named arguments: + +```twig +{{ stringDate | date(existingFormat="yyyy-MMMM-d", format="yyyy/MMMM/d") }} +``` + +When you are formatting an already parsed datetime, only `format` is usually needed. Use `existingFormat` when the source is still a plain string. + +#### Time zones + +Specify a target time zone when downstream systems require a local representation rather than UTC: + +```twig +{{ now() | date("yyyy-MM-dd'T'HH:mm:ssX", timeZone="UTC") }} +``` + +Supported arguments include: + +- `format` +- `existingFormat` +- `timeZone` +- `locale` + +#### `dateAdd` + +Adds or subtracts time from a date. Arguments: + +- `amount`: integer specifying how much to add or subtract +- `unit`: time unit such as `DAYS`, `HOURS`, `MONTHS`, or `YEARS` + +```twig +{{ now() | dateAdd(-1, 'DAYS') }} +``` + +#### Timestamp helpers + +Convert a date to a Unix timestamp at a specific precision: + +- `timestamp` — seconds +- `timestampMilli` — milliseconds +- `timestampMicro` — microseconds +- `timestampNano` — nanoseconds + +:::alert{type="warning"} +`timestampMicro` previously returned a nanosecond-precision value due to a bug. If you are migrating an older flow, verify the precision your downstream system expects. +::: + +All timestamp filters accept the same arguments as the `date` filter: `existingFormat` and `timeZone`. + +```twig +{{ now() | timestamp(timeZone="Europe/Paris") }} +{{ now() | timestampMilli(timeZone="Asia/Kolkata") }} +``` + +Supported date formats include standard Java `DateTimeFormatter` patterns and shortcuts such as `iso`, `sql`, `iso_date_time`, and `iso_zoned_date_time`. + +#### Temporal worked example + +```yaml +id: temporal_dates +namespace: company.team + +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 (milliseconds): {{ now() | timestampMilli(timeZone='Asia/Kolkata') }}" + - "Timezone (nanoseconds): {{ now() | timestampNano(timeZone='Asia/Kolkata') }}" +``` + +This kind of example is a good sanity check when you are validating timestamp precision before sending values to an external API. + +### YAML filters + +Use YAML filters when you are generating configuration or manifest-style text inside a task. They are less common in simple flows, but very useful in templated Kubernetes, Docker, or config-management patterns. + +#### `yaml` + +Parse YAML into an object: + +```twig +{{ "foo: bar" | yaml }} +``` + +This is especially useful in templated tasks where the source data starts as text but later expressions need object-style access. + +##### Example: using `yaml` 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` and `nindent` + +Useful when generating templated YAML or embedding structured content: + +```twig +{{ labels | yaml | indent(4) }} +{{ variables.yaml_data | yaml | nindent(4) }} +``` + +##### 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) }} +``` + +Use `indent` when the first line is already in place and only following lines need alignment. Use `nindent` when you need to start a fresh indented block on the next line. + +### Choosing the right filter quickly + +| If you need to... | Use | +| --- | --- | +| Parse or transform JSON payloads | `toJson`, `jq`, `first` | +| Provide a fallback string or value | `default` | +| Format a date | `date` | +| Offset a date | `dateAdd` | +| Split or join text | `split`, `join` | +| Normalize casing | `lower`, `upper`, `title`, `capitalize` | +| Convert a value to a string | `string` | +| Sort a collection | `sort`, `rsort` | +| Count items in a collection | `length` | +| Get unique values | `distinct` | +| Encode or decode Base64 | `base64encode`, `base64decode` | +| Hash a string | `sha1`, `sha512`, `md5` | +| Convert to a number | `number` | +| Render YAML in a templated task | `yaml`, `indent`, `nindent` | + +## Function Reference + +Use functions when you need to generate or retrieve a value dynamically with syntax such as `{{ functionName(...) }}`. + +### Common function groups + +Functions are best thought of as helpers that either fetch something, compute something, or force evaluation behavior that plain variables and filters cannot provide on their own. + +#### Rendering and debugging + +This group matters when expressions stop behaving the way you expect. `render()` and `printContext()` are often the quickest way to understand whether a value is missing, nested, or still just a string. + +- `render()` evaluates nested Pebble expressions +- `renderOnce()` renders a value only once +- `printContext()` outputs the full available context for debugging + +Examples: + +```twig +{{ render("{{ trigger.date ?? execution.startDate | date('yyyy-MM-dd') }}") }} +{{ printContext() }} +``` + +`renderOnce()` is the safer choice when you need one extra evaluation pass but do not want recursive expansion to keep walking nested Pebble content. + +#### Secrets and file access + +These functions bridge expressions to external or stored data. Use them when the value is not already present in the execution context and must be resolved at runtime. + +- `secret()` reads a secret from Kestra's secret backend +- `credential()` reads a short-lived token from a managed EE credential +- `read()` reads the contents of a namespace file or internal-storage file +- `fileURI()` returns the internal URI of a namespace file without reading its contents — use this when a task parameter expects a URI rather than inline content +- `kv(key, namespace, errorOnMissing)` reads a value from the KV store; `namespace` defaults to the flow's namespace and `errorOnMissing` defaults to `true` +- `encrypt(key, plaintext)` and `decrypt(key, encrypted)` encrypt and decrypt values using Kestra's encryption service + +Examples: + +```twig +{{ secret('GITHUB_ACCESS_TOKEN') }} +{{ credential('my_oauth') }} +{{ read('subdir/file.txt') }} +{{ fileURI('my_file.txt') }} +{{ kv('MY_KEY') }} +{{ kv('MY_KEY', 'other.namespace', false) }} +{{ encrypt('MY_SECRET_KEY', inputs.sensitiveValue) }} +{{ decrypt('MY_SECRET_KEY', outputs.encryptTask.value) }} +``` + +`read()` accepts both namespace files and internal-storage URIs, which makes it useful after download or transformation tasks that write files as outputs. Use `fileURI()` instead when you need to pass the file reference itself to a downstream task rather than embed the content inline. + +#### Data parsing helpers + +These helpers are most useful when a task output is still a serialized string and you want to treat it like structured data in later expressions. + +- `fromJson()` +- `fromIon()` +- `yaml()` + +Examples: + +```twig +{{ fromJson('[1, 2, 3]')[0] }} +{{ fromIon(read(outputs.serialize.uri)).someField }} +{{ yaml('foo: [666, 1, 2]').foo[0] }} +``` + +#### Execution and workflow helpers + +This group is more situational, but it becomes valuable in complex flows where you need to inspect sibling results, build links back into Kestra, or summarize failures. + +- `errorLogs()` for error summaries in alerts +- `currentEachOutput()` for simpler access to sibling outputs inside `ForEach` +- `tasksWithState()` returns a list of task run objects matching the given state — useful for building conditional logic or failure summaries based on task outcomes +- `iterationOutput(taskId, iteration)` retrieves the output of a specific iteration from a previous task; both arguments are optional and default to the current task and previous iteration +- `parentOutput(index)` retrieves the output of a parent task; `index` is optional and defaults to the direct parent +- `appLink()` in Enterprise Edition to generate Kestra App URLs + +#### Utility helpers + +- `now()` — returns the current datetime; accepts a `timeZone` argument: `now(timeZone="Europe/Paris")` +- `max(a, b, ...)` — returns the largest of its arguments +- `min(a, b, ...)` — returns the smallest of its arguments +- `range(start, end)` or `range(start, end, step)` — generates a list of integers up to and including `end`; the step defaults to 1 +- `uuid()` — generates a UUID in URL-safe base62 encoding +- `id()` — generates a short unique ID using Kestra's internal ID utility +- `ksuid()` — generates a K-Sortable Unique Identifier (timestamp-prefixed, base62-encoded); useful when sort order by creation time matters +- `nanoId(length, alphabet)` — generates a NanoID; `length` defaults to 21 and `alphabet` defaults to alphanumeric plus `-_` +- `randomInt(min, max)` — generates a random integer; the upper bound is **excluded** +- `randomPort()` — picks an available local port; useful in test or dev container flows +- `http(uri, ...)` — fetches a remote payload directly from an expression +- `fileSize(uri)` — returns the size in bytes of a file from internal storage +- `fileExists(uri)` — returns `true` if the file exists +- `isFileEmpty(uri)` — returns `true` if the file has no content + +#### Date and calendar helpers + +Use these functions when you need to make scheduling or routing decisions based on the calendar. + +- `isWeekend(date)` — returns `true` if the date falls on Saturday or Sunday +- `isPublicHoliday(date, countryCode, subDivision)` — returns `true` if the date is a public holiday; `countryCode` is an ISO 3166-1 alpha-2 code and `subDivision` (optional) is an ISO 3166-2 code +- `isDayWeekInMonth(date, dayOfWeek, position)` — returns `true` if the date is the Nth occurrence of the given weekday in its month; `position` accepts `FIRST`, `SECOND`, `THIRD`, `FOURTH`, or `LAST` +- `dayOfWeek(date)` — returns the uppercase day name such as `MONDAY` +- `dayOfMonth(date)` — returns the day of the month as an integer (1–31) +- `monthOfYear(date)` — returns the month as an integer (1–12) +- `hourOfDay(date)` — returns the hour as an integer (0–23) + +#### Template inheritance helpers + +These are less common than runtime-oriented helpers, but they matter when you are using Pebble blocks and template inheritance directly. + +##### `block()` + +`block()` renders the contents of a named block multiple times. It is different from the Pebble `block` tag, which declares the block: + +```twig +{% block "post" %}content{% endblock %} + +{{ block("post") }} +``` + +##### `parent()` + +Use `parent()` inside an overriding block to include the original block content from the parent template: + +```twig +{% extends "parent.peb" %} + +{% block "content" %} +child content +{{ parent() }} +{% endblock %} +``` + +### Common function patterns + +The functions below are the ones most likely to shape a real flow. This section focuses on the practical cases where they change how you write expressions, not just what they do in isolation. + +#### `render()` + +Use `render()` when a variable itself contains Pebble and must be evaluated: + +```twig +{{ render(namespace.github.token) }} +``` + +Without `render()`, namespace or flow variables that contain Pebble are treated as plain strings. + +#### `secret()` + +Use `secret()` for sensitive values: + +```twig +{{ secret('API_KEY') }} +``` + +#### `credential()` + +In Enterprise Edition, use `credential()` to inject a short-lived token from a managed credential: + +```twig +{{ credential('my_oauth') }} +``` + +`credential()` returns the token only, while the credential definition itself is managed in the Kestra UI. + +#### `currentEachOutput()` + +Use it inside `ForEach` flows to avoid manual `taskrun.value` indexing: + +```twig +{{ currentEachOutput(outputs.make_data).values.data }} +``` + +#### `errorLogs()` + +Prints all error logs from the current execution: + +```twig +{{ errorLogs() }} +``` + +It is most useful in `errors` blocks, where you need a compact summary of what failed without manually traversing task state objects. + +#### `fromIon()` + +Use `fromIon()` when a previous task or serializer produces Ion rather than JSON: + +```twig +{{ fromIon(read(outputs.serialize.uri)).someField }} +``` + +#### `read()` + +`read()` is the simplest way to turn a file URI back into inline content for a later expression: + +```twig +{{ read(outputs.someTask.uri) }} +``` + +#### `renderOnce()` + +Equivalent to `render(expression, recursive=false)`: + +```twig +{{ renderOnce(namespace.github.token) }} +``` + +#### `printContext()` + +Outputs the full execution context as a string. Use it in the Debug Expression console to inspect every variable available at that point in the execution: + +```twig +{{ printContext() }} +``` + +This is the fastest way to discover the exact key names and structure of `inputs`, `outputs`, `trigger`, and other context variables when an expression is not resolving as expected. + +#### `fromJson()` + +Parses a JSON string into an object so you can access its fields with dot or bracket notation: + +```twig +{{ fromJson(outputs.myTask.value).name }} +{{ fromJson('[1, 2, 3]')[0] }} +``` + +Use `fromJson()` when a task output arrives as a serialized JSON string rather than a structured object. To go the other direction, use the `toJson` filter. + +#### Numeric and generation helpers + +```twig +{{ max(5, 10, 15) }} +{# output: 15 #} +{{ min(5, 10, 15) }} +{# output: 5 #} +{{ now() }} +{{ now(timeZone="Europe/Paris") }} +{{ range(0, 3) }} +{# output: [0, 1, 2, 3] #} +{{ range(0, 6, 2) }} +{# output: [0, 2, 4, 6] #} +{{ uuid() }} +{{ id() }} +{{ ksuid() }} +{{ nanoId() }} +{{ nanoId(length=10) }} +{{ randomInt(1, 10) }} +{# generates a random integer from 1 to 9 (10 is excluded) #} +``` + +#### File and runtime helpers + +These helpers are usually used in operational flows rather than day-to-day templating: + +```twig +{{ randomPort() }} +{{ fileSize(outputs.download.uri) }} +{{ fileExists(outputs.download.uri) }} +{{ isFileEmpty(outputs.download.uri) }} +``` + +`tasksWithState()` returns a list of task run objects matching the given state. Use it in error handlers or notifications to report which tasks failed: + +```twig +{{ tasksWithState('FAILED') }} +``` + +#### `http()` + +`http()` lets an expression fetch a remote payload directly: + +```twig +{{ http(uri = 'https://dummyjson.com/products/categories') | jq('.[].slug') }} +``` + +Use it sparingly. It is convenient for dynamic dropdowns and lightweight lookups, but task-level HTTP calls are usually easier to observe and retry. + +#### `appLink()` + +Enterprise Edition's `appLink()` builds links back to Kestra Apps: + +```twig +{{ appLink(appId='com.example.my-app') }} +{{ appLink(baseUrl=true) }} +``` + +Use it in notifications when you want recipients to jump directly into the related app rather than the generic flow UI. + +#### `kv()` + +Reads a value from the KV store by key. The namespace defaults to the flow's namespace; set `errorOnMissing` to `false` to return `null` instead of throwing when the key is absent: + +```twig +{{ kv('MY_KEY') }} +{{ kv('MY_KEY', 'other.namespace') }} +{{ kv('OPTIONAL_KEY', namespace, false) }} +``` + +#### `encrypt()` and `decrypt()` + +Encrypt and decrypt string values using Kestra's encryption service. Both require a `key` argument that identifies which encryption key to use: + +```twig +{{ encrypt('MY_ENCRYPTION_KEY', inputs.sensitiveValue) }} +{{ decrypt('MY_ENCRYPTION_KEY', outputs.encryptTask.value) }} +``` + +#### `iterationOutput()` + +Retrieves the output of a specific iteration from a previous task. Both arguments are optional — `taskId` defaults to the current task and `iteration` defaults to the previous iteration: + +```twig +{{ iterationOutput(outputs.myTask).value }} +{{ iterationOutput(outputs.myTask, 2).value }} +``` + +#### `parentOutput()` + +Retrieves the output of a parent task. The optional `index` argument specifies which ancestor to target; omitting it returns the direct parent's output: + +```twig +{{ parentOutput() }} +{{ parentOutput(1) }} +``` + +#### Date and calendar helpers + +Use these functions when you need to make scheduling or routing decisions based on the calendar — for example, skipping runs on weekends or public holidays. + +`isWeekend(date)` returns `true` if the date falls on Saturday or Sunday. `isPublicHoliday(date, countryCode, subDivision)` checks against a country's public holiday calendar; `subDivision` is optional and accepts ISO 3166-2 codes. `isDayWeekInMonth(date, dayOfWeek, position)` returns `true` if the date is the Nth occurrence of a weekday in its month; `position` accepts `FIRST`, `SECOND`, `THIRD`, `FOURTH`, or `LAST`. + +```twig +{{ isWeekend(trigger.date) }} +{{ isPublicHoliday(trigger.date, 'US') }} +{{ isPublicHoliday(trigger.date, 'DE', 'DE-BY') }} +{{ isDayWeekInMonth(trigger.date, 'MONDAY', 'FIRST') }} +``` + +`dayOfWeek(date)` returns the uppercase day name (e.g. `MONDAY`). `dayOfMonth(date)`, `monthOfYear(date)`, and `hourOfDay(date)` return the corresponding integer component: + +```twig +{{ dayOfWeek(trigger.date) }} +{{ dayOfMonth(trigger.date) }} +{{ monthOfYear(trigger.date) }} +{{ hourOfDay(execution.startDate) }} +``` + +### Worked example + +This flow uses several runtime functions together: `now()` for a timestamp, `uuid()` for a unique run identifier, `secret()` for a credential, and `render()` to evaluate a namespace variable containing Pebble: + +```yaml +id: function_reference_example +namespace: company.team + +tasks: + - id: log_context + type: io.kestra.plugin.core.log.Log + message: + - "Run ID: {{ uuid() }}" + - "Started at: {{ now() | date('yyyy-MM-dd HH:mm:ss') }}" + - "API key: {{ secret('MY_API_KEY') }}" + - "Config value: {{ render(namespace.my_config) }}" + +``` + +## Operators, Tags, and Tests + +Use this section for the control-flow side of Pebble — comparisons, logic operators, fallbacks, loop and conditional tags, and type tests. + +### Operators + +#### Comparisons + +Supported comparison operators: + +- `==` +- `!=` +- `<` +- `>` +- `<=` +- `>=` + +```twig +{% if execution.state == "SUCCESS" %} + Flow completed successfully. +{% endif %} + +{% if taskrun.attemptsCount >= 3 %} + Max retries reached. +{% endif %} +``` + +#### Logic and boolean checks + +Use: + +- `and` +- `or` +- `not` +- `is` +- `contains` + +Use parentheses to group expressions and make precedence explicit: + +```twig +{% if 2 is even and 3 is odd %} + ... +{% endif %} + +{% if (3 is not even) and (2 is odd or 3 is even) %} + ... +{% endif %} +``` + +#### `contains` + +Checks whether an item exists within a list, string, map, or array: + +```twig +{% if ["apple", "pear", "banana"] contains "apple" %} + ... +{% endif %} +``` + +For maps, `contains` checks for a matching key: + +```twig +{% if {"apple": "red", "banana": "yellow"} contains "banana" %} + ... +{% endif %} +``` + +To check for multiple items at once, pass a list on the right-hand side: + +```twig +{% if ["apple", "pear", "banana", "peach"] contains ["apple", "peach"] %} + ... +{% endif %} +``` + +`contains` also works inline in output expressions: + +```twig +{{ inputs.mainString contains inputs.subString }} +``` + +#### `isIn` + +Use `isIn` to test whether a value matches any item in a list. It reads more clearly than chaining multiple equality checks in `runIf`, SLAs, or alert conditions: + +```twig +{{ execution.state isIn ['SUCCESS', 'KILLED', 'CANCELLED'] }} +``` + +#### Math and concatenation + +Use: + +- `+`, `-`, `*`, `/`, `%` +- `~` for string concatenation + +Example: + +```twig +{{ "apple" ~ "pear" ~ "banana" }} +{{ 2 + 2 / (10 % 3) * (8 - 1) }} +``` + +#### Fallbacks and conditionals + +Use: + +- `??` for null-coalescing: returns the first non-null value +- `???` for undefined-coalescing: returns the right-hand side only when the left is undefined (not just null) +- `? :` for ternary expressions + +Examples: + +```twig +{{ foo ?? bar ?? "default" }} {# first non-null value #} +{{ foo ??? "default" }} {# only if foo is undefined #} +{{ foo == null ? bar : baz }} +{{ foo ?? bar ?? raise }} {# raises an exception if all are undefined #} +``` + +For detailed null vs undefined behavior, see the [Handling null and undefined values](../../15.how-to-guides/null-values/index.md) guide. + +#### Operator precedence + +Pebble operators are evaluated in this order: + +1. `.` +2. `|` +3. `%`, `/`, `*` +4. `-`, `+` +5. `==`, `!=`, `>`, `<`, `>=`, `<=` +6. `is`, `is not` +7. `and` +8. `or` + +### Tags + +Pebble tags are enclosed in `{% %}` and control template flow. + +#### `set` + +Defines a variable in the template context: + +```twig +{% set header = "Welcome Page" %} +{{ header }} +{# output: Welcome Page #} +``` + +#### `if` + +Evaluates conditional logic. Use `elseif` and `else` for multiple branches: + +```twig +{% if users is empty %} + No users available. +{% elseif users.length == 1 %} + One user found. +{% else %} + Multiple users found. +{% endif %} +``` + +#### `for` + +Iterates over arrays, maps, or any `java.lang.Iterable`. + +**Iterating over a list:** + +```twig +{% for user in users %} + {{ user.name }} lives in {{ user.city }}. +{% else %} + No users found. +{% endfor %} +``` + +The `else` block runs when the collection is empty. + +**Iterating over a map:** + +```twig +{% for entry in map %} + {{ entry.key }}: {{ entry.value }} +{% endfor %} +``` + +**Loop special variables:** + +Inside any `for` loop, Pebble provides a `loop` object with these properties: + +| Variable | Description | +| --- | --- | +| `loop.index` | Zero-based index of the current iteration | +| `loop.length` | Total number of items in the iterable | +| `loop.first` | `true` on the first iteration | +| `loop.last` | `true` on the last iteration | +| `loop.revindex` | Number of iterations remaining | + +Example: + +```twig +{% for user in users %} + {{ loop.index }}: {{ user.name }}{% if loop.last %} (last){% endif %} +{% endfor %} +``` + +#### `filter` + +Applies a filter to a block of content. Filters can be chained: + +```twig +{% filter upper %} + hello +{% endfilter %} +{# output: HELLO #} + +{% filter lower | title %} + hello world +{% endfilter %} +{# output: Hello World #} +``` + +#### `raw` + +Prevents Pebble from parsing its content — useful when you need to output literal `{{ }}` syntax: + +```twig +{% raw %}{{ user.name }}{% endraw %} +{# output: {{ user.name }} #} +``` + +#### `macro` + +Defines a reusable template snippet. Macros only have access to their own arguments by default: + +```twig +{% macro input(type="text", name, value="") %} + type: "{{ type }}", name: "{{ name }}", value: "{{ value }}" +{% endmacro %} + +{{ input(name="country") }} +{# output: type: "text", name: "country", value: "" #} +``` + +To access variables from the outer template context, pass `_context` explicitly: + +```twig +{% set foo = "bar" %} + +{% macro display(_context) %} + {{ _context.foo }} +{% endmacro %} + +{{ display(_context) }} +{# output: bar #} +``` + +#### `block` + +Defines a named, reusable template block. Use the `block()` function to render the block elsewhere: + +```twig +{% block "header" %} + Introduction +{% endblock %} + +{{ block("header") }} +``` + +### Tests + +Tests are used with `is` and `is not` to perform type and value checks. + +#### `defined` + +Checks whether a variable exists in the context (regardless of its value): + +```twig +{% if missing is not defined %} + Variable is not defined. +{% endif %} +``` + +#### `empty` + +Returns `true` when a variable is null, an empty string, an empty collection, or an empty map: + +```twig +{% if user.email is empty %} + No email on record. +{% endif %} +``` + +#### `null` + +Checks whether a variable is null: + +```twig +{% if user.email is null %} + ... +{% endif %} + +{% if name is not null %} + ... +{% endif %} +``` + +#### `even` and `odd` + +Check whether an integer is even or odd: + +```twig +{% if 2 is even %} + ... +{% endif %} + +{% if 3 is odd %} + ... +{% endif %} +``` + +#### `iterable` + +Returns `true` when a variable implements `java.lang.Iterable`. Use this to guard a `for` loop when the collection may not always be present: + +```twig +{% if users is iterable %} + {% for user in users %} + {{ user.name }} + {% endfor %} +{% endif %} +``` + +#### `json` + +Returns `true` when a variable is a valid JSON string: + +```twig +{% if '{"test": 1}' is json %} + ... +{% endif %} +``` + +#### `map` -## Browse expressions docs +Returns `true` when a variable is a map: - +```twig +{% if {"apple": "red", "banana": "yellow"} is map %} + ... +{% endif %} +``` diff --git a/src/contents/redirects/docs.yml b/src/contents/redirects/docs.yml index 46ac8a55c90..c79952c798b 100644 --- a/src/contents/redirects/docs.yml +++ b/src/contents/redirects/docs.yml @@ -88,6 +88,16 @@ to: "/docs/enterprise/scalability/apps" - regexp: "^/docs/quickstart/contributing" to: "/docs/contribute-to-kestra" +- regexp: "/docs/expressions/execution-context/?$" + to: "/docs/expressions/#execution-context-variables" +- regexp: "/docs/expressions/pebble-syntax/?$" + to: "/docs/expressions/#pebble-syntax" +- regexp: "/docs/expressions/filter-reference/?$" + to: "/docs/expressions/#filter-reference" +- regexp: "/docs/expressions/function-reference/?$" + to: "/docs/expressions/#function-reference" +- regexp: "/docs/expressions/operators-tags-tests/?$" + to: "/docs/expressions/#operators-tags-and-tests" - regexp: "/docs/administrator-guide/deployment/docker/" to: "/docs/installation/docker-compose" - regexp: "/docs/administrator-guide/deployment/manual/" From 184a5ca59ddc153e4dd24026b1cfc7e4bd1546db Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Mon, 13 Apr 2026 16:20:08 +0200 Subject: [PATCH 14/52] docs(runif-when): add migration guide --- .../{v.1.4.0 => v2.0.0}/index.mdx | 10 +-- .../v2.0.0/run-if-renamed-when/index.md | 76 +++++++++++++++++++ 2 files changed, 81 insertions(+), 5 deletions(-) rename src/contents/docs/11.migration-guide/{v.1.4.0 => v2.0.0}/index.mdx (62%) create mode 100644 src/contents/docs/11.migration-guide/v2.0.0/run-if-renamed-when/index.md diff --git a/src/contents/docs/11.migration-guide/v.1.4.0/index.mdx b/src/contents/docs/11.migration-guide/v2.0.0/index.mdx similarity index 62% rename from src/contents/docs/11.migration-guide/v.1.4.0/index.mdx rename to src/contents/docs/11.migration-guide/v2.0.0/index.mdx index c2f238a1514..7b2cbbf2207 100644 --- a/src/contents/docs/11.migration-guide/v.1.4.0/index.mdx +++ b/src/contents/docs/11.migration-guide/v2.0.0/index.mdx @@ -1,14 +1,14 @@ --- -title: 1.4.0 +title: 2.0.0 icon: /src/contents/docs/icons/migration-guide.svg -release: 1.4.0 -description: Migration guides and deprecated features for Kestra version 1.4.0. +release: 2.0.0 +description: Migration guides and deprecated features for Kestra version 2.0.0. --- import ChildCard from "~/components/docs/ChildCard.astro" -## 1.4.0 +## 2.0.0 -Deprecated features and migration guides for 1.4.0 and onwards. +Deprecated features and migration guides for 2.0.0 and onwards. \ No newline at end of file diff --git a/src/contents/docs/11.migration-guide/v2.0.0/run-if-renamed-when/index.md b/src/contents/docs/11.migration-guide/v2.0.0/run-if-renamed-when/index.md new file mode 100644 index 00000000000..d47244d4923 --- /dev/null +++ b/src/contents/docs/11.migration-guide/v2.0.0/run-if-renamed-when/index.md @@ -0,0 +1,76 @@ +--- +title: runIf Renamed to when on Tasks +sidebarTitle: runIf → when (Tasks) +icon: /src/contents/docs/icons/migration-guide.svg +release: 2.0.0 +editions: ["OSS", "EE"] +description: The task-level runIf property has been renamed to when in Kestra 2.0.0, aligning it with the when property introduced on triggers. +--- + +Kestra 2.0.0 unifies conditional execution under a single property name: `when`. + +- **Tasks** — `runIf` is renamed to `when`. +- **Triggers** — the `conditions` list is deprecated in favor of a new `when` Pebble expression string. + +Both properties behave the same way: the Pebble expression is rendered at runtime, and if the result is falsy (`false`, `0`, `-0`, or an empty string), the task is set to `SKIPPED` or the trigger does not fire. + +A deprecated alias keeps `runIf` functional in 2.0.0 so existing flows continue to parse without changes. The alias is scheduled for removal in a future version — update your flows now to avoid a hard break later. + +## Tasks: runIf → when + +### Before + +```yaml +tasks: + - id: conditional_task + type: io.kestra.plugin.core.debug.Return + format: "Hello World!" + runIf: "{{ inputs.run_task }}" +``` + +### After + +```yaml +tasks: + - id: conditional_task + type: io.kestra.plugin.core.debug.Return + format: "Hello World!" + when: "{{ inputs.run_task }}" +``` + +The behavior is identical — the same Pebble rendering and `SKIPPED` state logic apply. + +## Triggers: conditions → when + +The `conditions` list on triggers is deprecated. Replace it with a single `when` Pebble expression. + +### Before + +```yaml +triggers: + - id: on_success + type: io.kestra.plugin.core.trigger.Flow + conditions: + - type: io.kestra.plugin.core.condition.ExecutionStatus + in: + - SUCCESS +``` + +### After + +```yaml +triggers: + - id: on_success + type: io.kestra.plugin.core.trigger.Flow + when: "{{ trigger.executionStatus == 'SUCCESS' }}" +``` + +## Migration steps + +1. **Search your flows** for `runIf:` and replace each occurrence with `when:`. The property value and any Pebble expressions stay the same. +2. **Search your flows** for `conditions:` on trigger blocks and replace them with an equivalent `when:` Pebble expression. +3. **Validate** by saving the updated flows in the Kestra UI or via the API. + +:::alert{type="warning"} +The `runIf` alias will be removed in a future release. Flows that still use `runIf` will fail to parse after the alias is dropped. +::: From 217f939019b10cd1f616e5ecb21d35cb5fa0c70b Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Mon, 13 Apr 2026 16:31:32 +0200 Subject: [PATCH 15/52] docs(when): use when instead of conditions and runIf --- .../docs/03.tutorial/04.triggers/index.md | 5 +- .../docs/03.tutorial/06.errors/index.md | 9 +-- .../05.workflow-components/01.tasks/index.mdx | 2 +- .../04.variables/index.md | 6 +- .../06.outputs/index.md | 2 +- .../07.triggers/01.schedule-trigger/index.md | 31 +++------- .../20.afterexecution/index.md | 6 +- .../docs/09.ui/02.executions/index.md | 2 +- .../03.monitoring/index.md | 57 ++----------------- .../docs/14.best-practices/7.outputs/index.md | 2 +- .../docs/15.how-to-guides/alerting/index.md | 8 +-- .../ansible-config-drift/index.md | 2 +- .../multiplecondition-listener/index.md | 9 +-- .../subflow-executions/index.md | 2 +- src/contents/docs/expressions/index.mdx | 2 +- .../docs/use-cases/03.microservices/index.md | 8 +-- 16 files changed, 33 insertions(+), 120 deletions(-) diff --git a/src/contents/docs/03.tutorial/04.triggers/index.md b/src/contents/docs/03.tutorial/04.triggers/index.md index 6d54a470f5c..39dbf586ddf 100644 --- a/src/contents/docs/03.tutorial/04.triggers/index.md +++ b/src/contents/docs/03.tutorial/04.triggers/index.md @@ -39,10 +39,7 @@ triggers: - id: flow_trigger type: io.kestra.plugin.core.trigger.Flow - conditions: - - type: io.kestra.plugin.core.condition.ExecutionFlow - namespace: company.team - flowId: first_flow + when: "{{ trigger.namespace == 'company.team' and trigger.flowId == 'first_flow' }}" ``` :::alert{type="info"} diff --git a/src/contents/docs/03.tutorial/06.errors/index.md b/src/contents/docs/03.tutorial/06.errors/index.md index 88ac33ecad4..dcf7c33237b 100644 --- a/src/contents/docs/03.tutorial/06.errors/index.md +++ b/src/contents/docs/03.tutorial/06.errors/index.md @@ -144,14 +144,7 @@ tasks: triggers: - id: listen type: io.kestra.plugin.core.trigger.Flow - conditions: - - type: io.kestra.plugin.core.condition.ExecutionStatus - in: - - FAILED - - WARNING - - type: io.kestra.plugin.core.condition.ExecutionNamespace - namespace: company.team - prefix: true + when: "{{ trigger.executionStatus in ['FAILED', 'WARNING'] and trigger.namespace startsWith 'company.team' }}" ``` Adding this flow ensures you receive a Slack alert for any flow failure in the `company.team` namespace. diff --git a/src/contents/docs/05.workflow-components/01.tasks/index.mdx b/src/contents/docs/05.workflow-components/01.tasks/index.mdx index 73283738d99..a78a3d11825 100644 --- a/src/contents/docs/05.workflow-components/01.tasks/index.mdx +++ b/src/contents/docs/05.workflow-components/01.tasks/index.mdx @@ -58,7 +58,7 @@ All tasks share the following core properties: | `description` | Your custom [documentation](../../../05.workflow-components/15.descriptions/index.md) of what the task does | | `retry` | How often should the task be retried in case of a failure, and the [type of retry strategy](../../../05.workflow-components/12.retries/index.md) | | `timeout` | The [maximum time allowed](../../../05.workflow-components/13.timeout/index.md) for the task to complete expressed in [ISO 8601 Durations](https://en.wikipedia.org/wiki/ISO_8601#Durations) | -| `runIf` | Skip a task if the provided condition evaluates to false | +| `when` | Skip a task if the provided condition evaluates to false | | `disabled` | A boolean flag indicating whether the task is [disabled or not](../../../05.workflow-components/16.disabled/index.md); if set to `true`, the task will be skipped during the execution | | `workerGroup` | The [group of workers](../../07.enterprise/04.scalability/worker-group/index.md) (EE-only) that are eligible to execute the task; you can specify a `workerGroup.key` and a `workerGroup.fallback` (the default is `WAIT`) | | `allowFailure` | A boolean flag allowing to continue the execution even if this task fails | diff --git a/src/contents/docs/05.workflow-components/04.variables/index.md b/src/contents/docs/05.workflow-components/04.variables/index.md index d4e2ce2fd20..c36c240241a 100644 --- a/src/contents/docs/05.workflow-components/04.variables/index.md +++ b/src/contents/docs/05.workflow-components/04.variables/index.md @@ -232,11 +232,7 @@ triggers: backfill: start: 2023-11-11T00:00:00Z cron: "0 11 * * MON" # at 11:00 every Monday - conditions: # only first Monday of the month - - type: io.kestra.plugin.core.condition.DayWeekInMonth - date: "{{ trigger.date }}" - dayOfWeek: "MONDAY" - dayInMonth: "FIRST" + when: "{{ isDayWeekInMonth(trigger.date, 'MONDAY', 'FIRST') }}" # only first Monday of the month ``` diff --git a/src/contents/docs/05.workflow-components/06.outputs/index.md b/src/contents/docs/05.workflow-components/06.outputs/index.md index 9671b0875ac..198b02a0dc8 100644 --- a/src/contents/docs/05.workflow-components/06.outputs/index.md +++ b/src/contents/docs/05.workflow-components/06.outputs/index.md @@ -187,7 +187,7 @@ tasks: - id: main type: io.kestra.plugin.core.debug.Return format: Hello World! - runIf: "{{ inputs.run_task }}" + when: "{{ inputs.run_task }}" - id: fallback type: io.kestra.plugin.core.debug.Return diff --git a/src/contents/docs/05.workflow-components/07.triggers/01.schedule-trigger/index.md b/src/contents/docs/05.workflow-components/07.triggers/01.schedule-trigger/index.md index 057dde8b8fd..46e79dc8b59 100644 --- a/src/contents/docs/05.workflow-components/07.triggers/01.schedule-trigger/index.md +++ b/src/contents/docs/05.workflow-components/07.triggers/01.schedule-trigger/index.md @@ -53,11 +53,7 @@ triggers: - id: schedule type: io.kestra.plugin.core.trigger.Schedule cron: "0 11 * * 1" - conditions: - - type: io.kestra.plugin.core.condition.DayWeekInMonth - date: "{{ trigger.date }}" - dayOfWeek: "MONDAY" - dayInMonth: "FIRST" + when: "{{ isDayWeekInMonth(trigger.date, 'MONDAY', 'FIRST') }}" ``` A schedule that runs daily at midnight US Eastern time: @@ -98,24 +94,13 @@ You can use this expression to make your **manual execution work**: `{{ trigger. ## Schedule conditions -When a `cron` expression alone is not sufficient (e.g., only first Monday of the month, only weekends), you can refine schedules using `conditions`. +When a `cron` expression alone is not sufficient (e.g., only first Monday of the month, only weekends), you can refine schedules using a `when` Pebble expression. -You **must** use the `{{ trigger.date }}` expression on the property `date` of the current schedule. +You can use the `{{ trigger.date }}` expression to access the current schedule date within the `when` expression. The [date and calendar helper functions](/docs/expressions#date-and-calendar-helpers) in the expressions reference cover all available date functions such as `isDayWeekInMonth()`, `dayOfWeek()`, `isWeekend()`, and `isPublicHoliday()`. -This condition will be evaluated and `{{ trigger.previous }}` and `{{ trigger.next }}` will reflect the date **with** the conditions applied. +The `when` expression is evaluated and `{{ trigger.previous }}` and `{{ trigger.next }}` will reflect the date **with** the condition applied. -The list of core conditions that can be used are: - - - [DateTimeBetween](/plugins/core/conditions/io.kestra.plugin.core.condition.DateTimeBetween) - - [DayWeek](/plugins/core/conditions/io.kestra.plugin.core.condition.DayWeek) - - [DayWeekInMonth](/plugins/core/conditions/io.kestra.plugin.core.condition.DayWeekInMonth) - - [Not](/plugins/core/conditions/io.kestra.plugin.core.condition.Not) - - [Or](/plugins/core/conditions/io.kestra.plugin.core.condition.Or) - - [Weekend](/plugins/core/conditions/io.kestra.plugin.core.condition.Weekend) - - [PublicHoliday](/plugins/core/conditions/io.kestra.plugin.core.condition.publicholiday) - - [TimeBetween](/plugins/core/conditions/io.kestra.plugin.core.condition.timebetween) - -Here's an example using the `DayWeek` condition: +Here's an example using a day-of-week check: ```yaml id: conditions @@ -130,9 +115,7 @@ triggers: - id: schedule type: io.kestra.plugin.core.trigger.Schedule cron: "@hourly" - conditions: - - type: io.kestra.plugin.core.condition.DayWeek - dayOfWeek: "THURSDAY" + when: "{{ dayOfWeek(trigger.date) == 'THURSDAY' }}" ``` ## Recover missed schedules @@ -263,7 +246,7 @@ namespace: system tasks: - id: send_alert - runIf: "{{ trigger.data }}" + when: "{{ trigger.data }}" type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook url: https://kestra.io/api/mock messageText: The following Schedule triggers seem unhealthy {{ trigger.data }} diff --git a/src/contents/docs/05.workflow-components/20.afterexecution/index.md b/src/contents/docs/05.workflow-components/20.afterexecution/index.md index 7ba2394cb9e..58791dd4905 100644 --- a/src/contents/docs/05.workflow-components/20.afterexecution/index.md +++ b/src/contents/docs/05.workflow-components/20.afterexecution/index.md @@ -18,7 +18,7 @@ Run tasks after a flow execution completes. ## `afterExecution` property -`afterExecution` is a block of tasks that run after the flow ends. You can use it to run conditional tasks based on the final state, such as **SUCCESS** or **FAILED**. This is especially useful for custom notifications and alerts. For example, you can combine `afterExecution` with the `runIf` property to send different Slack messages depending on the execution state. +`afterExecution` is a block of tasks that run after the flow ends. You can use it to run conditional tasks based on the final state, such as **SUCCESS** or **FAILED**. This is especially useful for custom notifications and alerts. For example, you can combine `afterExecution` with the `when` property to send different Slack messages depending on the execution state. ```yaml id: alerts_demo @@ -30,13 +30,13 @@ tasks: afterExecution: - id: onSuccess - runIf: "{{execution.state == 'SUCCESS'}}" + when: "{{execution.state == 'SUCCESS'}}" type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook url: https://hooks.slack.com/services/xxxxx messageText: "{{flow.namespace}}.{{flow.id}} finished successfully!" - id: onFailure - runIf: "{{execution.state == 'FAILED'}}" + when: "{{execution.state == 'FAILED'}}" type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook url: https://hooks.slack.com/services/xxxxx messageText: "Oh no, {{flow.namespace}}.{{flow.id}} failed!!!" diff --git a/src/contents/docs/09.ui/02.executions/index.md b/src/contents/docs/09.ui/02.executions/index.md index 19a035b5d31..db40e2ec311 100644 --- a/src/contents/docs/09.ui/02.executions/index.md +++ b/src/contents/docs/09.ui/02.executions/index.md @@ -39,7 +39,7 @@ inputs: tasks: - id: taskA - runIf: "{{ inputs.runTask }}" + when: "{{ inputs.runTask }}" type: io.kestra.plugin.core.debug.Return format: Hello World! diff --git a/src/contents/docs/10.administrator-guide/03.monitoring/index.md b/src/contents/docs/10.administrator-guide/03.monitoring/index.md index e0cc8f46a6f..40b761e991c 100644 --- a/src/contents/docs/10.administrator-guide/03.monitoring/index.md +++ b/src/contents/docs/10.administrator-guide/03.monitoring/index.md @@ -51,14 +51,7 @@ tasks: triggers: - id: listen type: io.kestra.plugin.core.trigger.Flow - conditions: - - type: io.kestra.plugin.core.condition.ExecutionStatus - in: - - FAILED - - WARNING - - type: io.kestra.plugin.core.condition.ExecutionNamespace - namespace: company.analytics - prefix: true + when: "{{ trigger.executionStatus in ['FAILED', 'WARNING'] and trigger.namespace startsWith 'company.analytics' }}" ``` Adding this single flow will ensure that you receive a Slack alert on any flow failure in the `company.analytics` namespace. Here is an example alert notification: @@ -66,7 +59,7 @@ Adding this single flow will ensure that you receive a Slack alert on any flow f ![alert notification](../../03.tutorial/06.errors/alert-notification.png) :::alert{type="warning"} -Note that if you want this alert to be sent on failure across multiple namespaces, you will need to add an `OrCondition` to the `conditions` list. See the example below: +Note that if you want this alert to be sent on failure across multiple namespaces, combine conditions using `or` in the `when` Pebble expression. See the example below: ```yaml id: alert namespace: company.system @@ -81,53 +74,11 @@ tasks: triggers: - id: listen type: io.kestra.plugin.core.trigger.Flow - conditions: - - type: io.kestra.plugin.core.condition.ExecutionStatus - in: - - FAILED - - WARNING - - type: io.kestra.plugin.core.condition.Or - conditions: - - type: io.kestra.plugin.core.condition.ExecutionNamespace - namespace: company.product - prefix: true - - type: io.kestra.plugin.core.condition.ExecutionFlow - flowId: cleanup - namespace: company.system + when: "{{ trigger.executionStatus in ['FAILED', 'WARNING'] and (trigger.namespace startsWith 'company.product' or (trigger.flowId == 'cleanup' and trigger.namespace == 'company.system')) }}" ``` ::: -The example above works correctly. However, if you list the conditions without using `OrCondition`, no alerts will be sent because Kestra will try to match all conditions simultaneously. Since there’s no overlap between them, the conditions cancel each other out. See the example below: - -```yaml -id: bad_example -namespace: company.monitoring -description: This example will not work - -tasks: - - id: send - type: io.kestra.plugin.slack.notifications.SlackExecution - url: "{{ secret('SLACK_WEBHOOK') }}" - channel: "#general" - executionId: "{{trigger.executionId}}" - -triggers: - - id: listen - type: io.kestra.plugin.core.trigger.Flow - conditions: - - type: io.kestra.plugin.core.condition.ExecutionStatus - in: - - FAILED - - WARNING - - type: io.kestra.plugin.core.condition.ExecutionNamespace - namespace: company.product - prefix: true - - type: io.kestra.plugin.core.condition.ExecutionFlow - flowId: cleanup - namespace: company.system -``` - -Here, there's no overlap between the two conditions. The first condition will only match executions in the `company.product` namespace, while the second condition will only match executions from the `cleanup` flow in the `company.system` namespace. If you want to match executions from the `cleanup` flow in the `company.system` namespace **or** any execution in the `product` namespace, make sure to add the `OrCondition`. +The example above works correctly because `or` is used explicitly in the Pebble expression. If you combine two conditions with `and` where only one can ever be true at a time, no alerts will be sent. For example, a flow execution can only belong to one namespace at a time, so requiring it to match two different namespace prefixes simultaneously will never fire. Make sure to use `or` for alternatives and `and` only for conditions that can both be true at the same time. ## Monitoring diff --git a/src/contents/docs/14.best-practices/7.outputs/index.md b/src/contents/docs/14.best-practices/7.outputs/index.md index c7e1a6f03f8..43c9d729ab2 100644 --- a/src/contents/docs/14.best-practices/7.outputs/index.md +++ b/src/contents/docs/14.best-practices/7.outputs/index.md @@ -22,7 +22,7 @@ inputs: tasks: - id: taskA - runIf: "{{ inputs.runTask }}" + when: "{{ inputs.runTask }}" type: io.kestra.plugin.core.debug.Return format: Hello World! diff --git a/src/contents/docs/15.how-to-guides/alerting/index.md b/src/contents/docs/15.how-to-guides/alerting/index.md index 88bdd58f1dd..b7fbb8d607e 100644 --- a/src/contents/docs/15.how-to-guides/alerting/index.md +++ b/src/contents/docs/15.how-to-guides/alerting/index.md @@ -80,7 +80,7 @@ errors: ## Flow trigger -Subflows cut down on duplication, but you still need the `errors` block in every flow. For a fully centralized approach, use a **Flow trigger** that reacts to execution status. Trigger conditions let you target specific states, such as `FAILED` or `WARNING`, and you can define separate triggers per status if needed. +Subflows cut down on duplication, but you still need the `errors` block in every flow. For a fully centralized approach, use a **Flow trigger** that reacts to execution status. The `when` expression lets you target specific states, such as `FAILED` or `WARNING`, and you can define separate triggers per status if needed. ```yaml id: failure_alert_slack @@ -96,11 +96,7 @@ tasks: triggers: - id: on_failure type: io.kestra.plugin.core.trigger.Flow - conditions: - - type: io.kestra.plugin.core.condition.ExecutionStatus - in: - - FAILED - - WARNING + when: "{{ trigger.executionStatus in ['FAILED', 'WARNING'] }}" ``` diff --git a/src/contents/docs/15.how-to-guides/ansible-config-drift/index.md b/src/contents/docs/15.how-to-guides/ansible-config-drift/index.md index 76fa5e55bd5..4eb9e2e34c9 100644 --- a/src/contents/docs/15.how-to-guides/ansible-config-drift/index.md +++ b/src/contents/docs/15.how-to-guides/ansible-config-drift/index.md @@ -75,7 +75,7 @@ tasks: tasks: - id: check_drift type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook - runIf: "{{ taskrun.value | jq('.changed') | first == true }}" + when: "{{ taskrun.value | jq('.changed') | first == true }}" url: "{{ secret('SLACK_WEBHOOK') }}" payload: | { diff --git a/src/contents/docs/15.how-to-guides/multiplecondition-listener/index.md b/src/contents/docs/15.how-to-guides/multiplecondition-listener/index.md index 86e1a5a9392..c7e813f8e5a 100644 --- a/src/contents/docs/15.how-to-guides/multiplecondition-listener/index.md +++ b/src/contents/docs/15.how-to-guides/multiplecondition-listener/index.md @@ -93,12 +93,9 @@ tasks: triggers: - id: multiple_listen_flow type: io.kestra.plugin.core.trigger.Flow - conditions: - - type: io.kestra.plugin.core.condition.ExecutionStatus - in: - - SUCCESS + when: "{{ trigger.executionStatus == 'SUCCESS' }}" + multipleConditions: - id: multiple - type: io.kestra.plugin.core.condition.MultipleCondition window: P1D windowAdvance: P0D conditions: @@ -123,7 +120,7 @@ triggers: - The `multiple_listen_flow` trigger listens for both `multiplecondition_flow_a` and `multiplecondition_flow_b`. - - Execution Status Condition: Ensures that only successful executions (status `SUCCESS`) are considered. + - The `when` expression ensures that only successful executions (status `SUCCESS`) are considered. - MultipleCondition: This condition checks that both `flow_a` and `flow_b` have successfully completed within the last 24 hours (`P1D`). 3. Window: diff --git a/src/contents/docs/15.how-to-guides/subflow-executions/index.md b/src/contents/docs/15.how-to-guides/subflow-executions/index.md index 6d91d1fe20d..bd8015a4e8d 100644 --- a/src/contents/docs/15.how-to-guides/subflow-executions/index.md +++ b/src/contents/docs/15.how-to-guides/subflow-executions/index.md @@ -60,7 +60,7 @@ tasks: - id: fail type: io.kestra.plugin.core.execution.Fail - runIf: "{{ randomInt(lower=0, upper=2) == 1 }}" + when: "{{ randomInt(lower=0, upper=2) == 1 }}" errorMessage: Bad value returned! - id: end diff --git a/src/contents/docs/expressions/index.mdx b/src/contents/docs/expressions/index.mdx index ddc59e0d3bc..bf67793d5aa 100644 --- a/src/contents/docs/expressions/index.mdx +++ b/src/contents/docs/expressions/index.mdx @@ -1451,7 +1451,7 @@ To check for multiple items at once, pass a list on the right-hand side: #### `isIn` -Use `isIn` to test whether a value matches any item in a list. It reads more clearly than chaining multiple equality checks in `runIf`, SLAs, or alert conditions: +Use `isIn` to test whether a value matches any item in a list. It reads more clearly than chaining multiple equality checks in `when`, SLAs, or alert conditions: ```twig {{ execution.state isIn ['SUCCESS', 'KILLED', 'CANCELLED'] }} diff --git a/src/contents/docs/use-cases/03.microservices/index.md b/src/contents/docs/use-cases/03.microservices/index.md index aaef24a4041..83227acfe45 100644 --- a/src/contents/docs/use-cases/03.microservices/index.md +++ b/src/contents/docs/use-cases/03.microservices/index.md @@ -57,25 +57,25 @@ tasks: - id: processPayment type: io.kestra.plugin.core.http.Request - runIf: "{{ outputs.checkInventory.code == 201 }}" + when: "{{ outputs.checkInventory.code == 201 }}" description: Process payment for the order uri: https://kestra.io/api/mock - id: orderConfirmation type: io.kestra.plugin.core.http.Request - runIf: "{{ outputs.processPayment.code == 201 }}" + when: "{{ outputs.processPayment.code == 201 }}" description: Confirm the order and notify the customer uri: https://kestra.io/api/mock - id: arrangeShipping type: io.kestra.plugin.core.http.Request - runIf: "{{ outputs.orderConfirmation.code == 201 }}" + when: "{{ outputs.orderConfirmation.code == 201 }}" description: Arrange shipping for the order uri: https://kestra.io/api/mock - id: updateDeliveryStatus type: io.kestra.plugin.core.http.Request - runIf: "{{ outputs.arrangeShipping.code == 201 }}" + when: "{{ outputs.arrangeShipping.code == 201 }}" description: Update the delivery status of the order uri: https://kestra.io/api/mock From 6ea6be45004771b6719c96742c53e0f5b96481ac Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Mon, 13 Apr 2026 16:43:22 +0200 Subject: [PATCH 16/52] docs(TRACEPARENT): add tracing for scripts Part of https://github.com/kestra-io/kestra-ee/issues/6826 --- .../open-telemetry/index.md | 29 +++++++++++++++++++ src/contents/docs/expressions/index.mdx | 1 + 2 files changed, 30 insertions(+) diff --git a/src/contents/docs/10.administrator-guide/open-telemetry/index.md b/src/contents/docs/10.administrator-guide/open-telemetry/index.md index 5af09fc2e71..80c37e67dbc 100644 --- a/src/contents/docs/10.administrator-guide/open-telemetry/index.md +++ b/src/contents/docs/10.administrator-guide/open-telemetry/index.md @@ -55,6 +55,35 @@ Kestra propagates the trace context so that traces are correlated: - Flow execution traces correlate with parent flows when the `Subflow` or `ForEachItem` task is used. - External HTTP calls include the standard propagation header for downstream correlation. +### Propagate trace context to scripts + +Scripts run in isolated containers, so OTel spans they generate start a new root trace by default. Pass `{{ trace.parent }}` as the `TRACEPARENT` environment variable to parent those spans under the Kestra task span. + +`{{ trace.parent }}` holds the W3C [traceparent](https://www.w3.org/TR/trace-context/) header; it is empty when tracing is disabled. + +```yaml +id: traced_script +namespace: company.team + +tasks: + - id: run_python + type: io.kestra.plugin.scripts.python.Script + env: + TRACEPARENT: "{{ trace.parent }}" + script: | + from opentelemetry.propagate import extract + from opentelemetry.sdk.trace import TracerProvider + import os + + ctx = extract({"traceparent": os.environ.get("TRACEPARENT", "")}) + tracer = TracerProvider().get_tracer(__name__) + + with tracer.start_as_current_span("my-span", context=ctx): + pass # spans here appear as children of the Kestra task span +``` + +`TRACEPARENT` is recognized by all major OTel SDKs and works the same way for Node.js, Bash, and any other script type. The variable is also usable in HTTP task headers and any other [expression-capable property](../../expressions/index.md#default-execution-context-variables). + ### Example: Jaeger with Docker Compose Enable [Jaeger](https://www.jaegertracing.io), an OpenTelemetry-compatible tracing platform, with Kestra in a Docker Compose configuration file: diff --git a/src/contents/docs/expressions/index.mdx b/src/contents/docs/expressions/index.mdx index bf67793d5aa..0d683c7aabe 100644 --- a/src/contents/docs/expressions/index.mdx +++ b/src/contents/docs/expressions/index.mdx @@ -77,6 +77,7 @@ The Debug Expression console is available in the Kestra UI under **Executions | `{{ parent.outputs }}` | Outputs of the nearest parent task run | | `{{ parents }}` | List of parent task runs | | `{{ labels }}` | Execution labels accessible by key | +| `{{ trace.parent }}` | W3C `traceparent` header for the current execution; only populated when [OpenTelemetry tracing is enabled](../10.administrator-guide/open-telemetry/index.md#traces) | Example: From 47a23892c6d7b4c592b8aa6aac8d36ef9a326dbf Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Thu, 16 Apr 2026 11:35:24 +0200 Subject: [PATCH 17/52] docs(tls/mtls): update enterprise config Part of https://github.com/kestra-io/kestra-ee/issues/7422 --- .../security-hardening/index.md | 9 ++ .../06.enterprise-and-advanced/index.md | 146 +++++++++++++++++- 2 files changed, 154 insertions(+), 1 deletion(-) diff --git a/src/contents/docs/10.administrator-guide/security-hardening/index.md b/src/contents/docs/10.administrator-guide/security-hardening/index.md index d15130b9056..e9a15b487d5 100644 --- a/src/contents/docs/10.administrator-guide/security-hardening/index.md +++ b/src/contents/docs/10.administrator-guide/security-hardening/index.md @@ -23,6 +23,15 @@ Running workflows in isolated environments reduces the impact of potential malic - **Ephemeral compute** — use Kestra's native [Task Runners](../../07.enterprise/04.scalability/task-runners/index.md) to auto-scale ephemeral compute nodes, which are destroyed after each run to ensure no residual state. - **Minimum host permissions** - grant only the OS-level rights required for the runtime; avoid mounting cloud credential files or granting host-level IAM roles directly. +## Transport security (EE only) + +In distributed deployments, Worker Controllers communicate with Workers over gRPC. By default this channel is plaintext. Enterprise Edition supports TLS encryption and mutual TLS (mTLS) to authenticate both sides of the connection: + +- **One-way TLS** — the controller presents a certificate; workers verify it. Encrypts the channel without requiring worker certificates. +- **Mutual TLS (mTLS)** — both controller and worker present certificates. Use this when you need strong identity verification between components, not just encryption. + +See [gRPC TLS/mTLS configuration](../../configuration/06.enterprise-and-advanced/index.md#grpc-tlsmtls-ee-only) for setup instructions and a full property reference. + ## Plugin and code validation To prevent the execution of malicious code, you can implement several strategies: diff --git a/src/contents/docs/configuration/06.enterprise-and-advanced/index.md b/src/contents/docs/configuration/06.enterprise-and-advanced/index.md index 4607aece913..2c2784a412a 100644 --- a/src/contents/docs/configuration/06.enterprise-and-advanced/index.md +++ b/src/contents/docs/configuration/06.enterprise-and-advanced/index.md @@ -1,6 +1,6 @@ --- title: Kestra Enterprise and Advanced Configuration -description: Configure Enterprise-only and advanced Kestra settings including licenses, Elasticsearch, Kafka, indexer behavior, UI custom links, AI Copilot, and air-gapped deployments. +description: Configure Enterprise-only and advanced Kestra settings including licenses, gRPC TLS/mTLS, Elasticsearch, Kafka, indexer behavior, UI custom links, AI Copilot, and air-gapped deployments. sidebarTitle: Enterprise and Advanced icon: /src/contents/docs/icons/admin.svg editions: ["EE", "Cloud"] @@ -16,6 +16,7 @@ This area includes: - Enterprise license configuration - Enterprise Java security +- gRPC TLS/mTLS for worker ↔ controller communication - UI sidebar customization - historical multi-tenancy and default tenant settings - custom links in the UI @@ -87,6 +88,149 @@ kestra: The old multi-tenancy and default-tenant configuration was removed in `0.23.0`; keep it only in mind for migration work. +## gRPC TLS/mTLS (EE only) + +Use this section when running Kestra in a distributed topology where the Worker Controller and Workers communicate over gRPC and you need to encrypt that channel. By default, gRPC traffic is plaintext. Enabling TLS here encrypts the controller ↔ worker channel; enabling mTLS additionally requires workers to present a certificate the controller trusts. + +This feature is active on any component with server type `CONTROLLER`, `WORKER`, or `STANDALONE`. + +### One-way TLS + +The controller presents a certificate; workers verify it against a truststore. Configure the controller (server side) with a keystore and the workers (client side) with a matching truststore: + +**Controller:** + +```yaml +kestra: + grpc: + tls: + enabled: true + key-store: + path: /etc/kestra/tls/controller-keystore.p12 + type: PKCS12 + password: "" +``` + +**Worker:** + +```yaml +kestra: + grpc: + tls: + enabled: true + trust-store: + path: /etc/kestra/tls/ca-truststore.p12 + type: PKCS12 + password: "" +``` + +If no truststore is provided on the worker side, the JVM default trust store is used. This is appropriate when the controller certificate is signed by a well-known CA. + +### Mutual TLS (mTLS) + +Set `client-auth: REQUIRE` on the controller to enforce that workers present a certificate. Both sides need a keystore and a truststore: + +**Controller:** + +```yaml +kestra: + grpc: + tls: + enabled: true + client-auth: REQUIRE + key-store: + path: /etc/kestra/tls/controller-keystore.p12 + type: PKCS12 + password: "" + trust-store: + path: /etc/kestra/tls/ca-truststore.p12 + type: PKCS12 + password: "" +``` + +**Worker:** + +```yaml +kestra: + grpc: + tls: + enabled: true + key-store: + path: /etc/kestra/tls/worker-keystore.p12 + type: PKCS12 + password: "" + trust-store: + path: /etc/kestra/tls/ca-truststore.p12 + type: PKCS12 + password: "" +``` + +`client-auth` also accepts `OPTIONAL`, which requests a client certificate but does not require one. + +### Authority override for static discovery + +When using static discovery, the gRPC channel authority is the synthetic value `controllers` rather than a real hostname. If the controller certificate's Subject Alternative Names (SANs) do not include `controllers`, TLS verification will fail. Set `authority-override` on the worker to a hostname that is present in the certificate's SANs: + +```yaml +kestra: + grpc: + tls: + enabled: true + authority-override: kestra-controller + trust-store: + path: /etc/kestra/tls/ca-truststore.p12 + type: PKCS12 + password: "" +``` + +This is not needed with DNS-based discovery, where the authority is derived from the actual hostname. + +### JKS keystores + +PKCS12 is the recommended format. For JKS keystores, set `type: JKS`. JKS also supports a separate key password (used when the private key entry password differs from the store password): + +```yaml +kestra: + grpc: + tls: + enabled: true + key-store: + path: /etc/kestra/tls/keystore.jks + type: JKS + password: "" + key-password: "" +``` + +### Development: skip certificate verification + +:::alert{type="warning"} +`insecure-trust-all-certificates: true` disables CA verification entirely. Use only in local development or CI environments where certificates are self-signed and not managed. Never enable this in production. +::: + +```yaml +kestra: + grpc: + tls: + enabled: true + insecure-trust-all-certificates: true +``` + +### Configuration reference + +| Property | Default | Description | +| --- | --- | --- | +| `kestra.grpc.tls.enabled` | `false` | Enable TLS for gRPC communication | +| `kestra.grpc.tls.key-store.path` | — | Path to keystore file | +| `kestra.grpc.tls.key-store.type` | `PKCS12` | Keystore format (`PKCS12` or `JKS`) | +| `kestra.grpc.tls.key-store.password` | — | Keystore password | +| `kestra.grpc.tls.key-store.key-password` | — | Private key entry password (JKS only) | +| `kestra.grpc.tls.trust-store.path` | — | Path to truststore file | +| `kestra.grpc.tls.trust-store.type` | `PKCS12` | Truststore format | +| `kestra.grpc.tls.trust-store.password` | — | Truststore password | +| `kestra.grpc.tls.client-auth` | `NONE` | Client auth mode: `NONE`, `OPTIONAL`, or `REQUIRE` | +| `kestra.grpc.tls.insecure-trust-all-certificates` | `false` | Skip CA verification (development only) | +| `kestra.grpc.tls.authority-override` | — | Override TLS authority for static discovery | + ## Elasticsearch, Kafka, and indexing This section is really about one architectural choice: running Kestra on the Kafka plus Elasticsearch stack instead of the simpler JDBC-backed setup. If you are on PostgreSQL or MySQL only, much of this page will not apply. From 3cc73419a6cece2f1c876cfaed3ec60b898680c4 Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Fri, 17 Apr 2026 09:56:43 +0200 Subject: [PATCH 18/52] docs(ai-agents): mention usage metric emits Part of https://github.com/kestra-io/plugin-ai/issues/297 --- src/contents/docs/ai-tools/ai-agents/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contents/docs/ai-tools/ai-agents/index.md b/src/contents/docs/ai-tools/ai-agents/index.md index e843eefe8d3..cd5680720b4 100644 --- a/src/contents/docs/ai-tools/ai-agents/index.md +++ b/src/contents/docs/ai-tools/ai-agents/index.md @@ -119,7 +119,7 @@ Following `multilingual_agent` is the `english_brevity` task, which only needs a ![AI Agent Abbreviated Summary](./ai-agent-brevity.png) -These outputs can then be passed on as notifications or system messages to external tools or subflows within Kestra. Other useful outputs include `tokenUsage` to compare different providers for the same tasks. For more examples and details about properties, outputs, and definitions, refer to the AI [Agent plugin documentation](/plugins/plugin-ai/agent). +These outputs can then be passed on as notifications or system messages to external tools or subflows within Kestra. Other useful outputs include `tokenUsage` to compare different providers for the same tasks. At runtime, Kestra also emits counter metrics — `ai.agent.tool.calls`, `ai.provider.calls`, and `ai.embedding.store.calls` — tagged by class name, which you can scrape with Prometheus or export via OpenTelemetry to monitor AI task usage. For more examples and details about properties, outputs, and definitions, refer to the AI [Agent plugin documentation](/plugins/plugin-ai/agent). ### Plugin defaults From f30039ad8c24ca740f72c6a4aada40a4f22d27e1 Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Mon, 20 Apr 2026 11:02:44 +0200 Subject: [PATCH 19/52] docs(ldap): add mode property controls Part of https://github.com/kestra-io/kestra-ee/issues/4961 --- .../07.enterprise/03.auth/sso/ldap/index.md | 76 ++++++++++++++++++- 1 file changed, 72 insertions(+), 4 deletions(-) diff --git a/src/contents/docs/07.enterprise/03.auth/sso/ldap/index.md b/src/contents/docs/07.enterprise/03.auth/sso/ldap/index.md index 5cc2e82d8a4..fd56249209f 100644 --- a/src/contents/docs/07.enterprise/03.auth/sso/ldap/index.md +++ b/src/contents/docs/07.enterprise/03.auth/sso/ldap/index.md @@ -1,7 +1,7 @@ --- -title: "LDAP Authentication in Kestra: Directory Login" +title: "LDAP Authentication in Kestra: Directory Login and Group Sync" h1: Connect Your LDAP Directory for User Login and Group Sync -description: Enable LDAP authentication in Kestra. Connect your existing LDAP directory to manage user login and group synchronization securely. +description: Enable LDAP authentication in Kestra. Use your LDAP directory for user login, group synchronization, or both — including alongside an existing SSO provider. sidebarTitle: LDAP icon: /src/contents/docs/icons/admin.svg editions: ["EE"] @@ -10,10 +10,18 @@ version: "0.22.0" Enable LDAP authentication in Kestra to authenticate users against your existing directory and sync group memberships automatically. +## Configure LDAP authentication + +Enable LDAP authentication in Kestra to authenticate users against your existing directory, sync group memberships, or both. You can also use LDAP solely for group sync while keeping an existing SSO provider for login. +
+:::alert{type="warning"} +LDAP is a licensed feature. If `micronaut.security.ldap.default` is configured but your license does not include LDAP, Kestra will refuse to start with the error: `LDAP is not supported by your license`. Contact your Kestra account team to enable it. +::: + ## What is LDAP Lightweight directory access protocol (LDAP) allows applications to quickly query user information. Organizations use directories to store usernames, passwords, email addresses, and other static data. LDAP is an open, vendor-neutral protocol for accessing and managing that data. @@ -22,12 +30,22 @@ With Kestra, you can use an existing LDAP directory to authenticate users and sy ## Configuration -LDAP is configured under the security context of your [Kestra Security and Secrets configuration](../../../../configuration/05.security-and-secrets/index.md) file. +LDAP is configured under the security context of your [Kestra Security and Secrets configuration](../../../../configuration/05.security-and-secrets/index.md) file. [LDAP with Micronaut](https://micronaut-projects.github.io/micronaut-security/4.11.3/guide/#ldap) supports `context`, `search`, and `groups` as core configuration properties supported out of the box. These properties define the connection context, user attribute mapping, and group filtering needed to synchronize users and their group memberships with Kestra. The `user-attributes` section maps LDAP attributes such as `givenName`, `sn`, and `mail` to the corresponding Kestra user properties (first name, last name, and email). +Below are example configurations with Kestra-specific properties on top of the Micronaut configuration. + +The `mode` property controls how Kestra uses the LDAP connection: + +| Mode | Description | +|---|---| +| `AUTHENTICATION` | LDAP handles user login only. No group sync. **This is the default.** | +| `AUTHENTICATION_AND_GROUP_SYNC` | LDAP handles both user login and group membership sync. | +| `GROUP_SYNC_ONLY` | LDAP is used only to resolve group memberships. Users log in via an existing SSO provider. | + The examples below extend the base Micronaut LDAP configuration with these Kestra-specific mappings. ### Unix configuration @@ -37,6 +55,7 @@ micronaut: security: ldap: default: + mode: AUTHENTICATION_AND_GROUP_SYNC # or AUTHENTICATION to skip group sync user-attributes: firstName: givenName lastName: sn @@ -58,6 +77,7 @@ micronaut: base: "ou=groups,dc=example,dc=org" filter: "{&(objectClass=posixGroup)(memberUid={0})}" filter-attribute: uid + attribute: cn ``` ### Windows configuration @@ -68,6 +88,7 @@ micronaut: ldap: default: enabled: true + mode: AUTHENTICATION_AND_GROUP_SYNC # or AUTHENTICATION to skip group sync user-attributes: firstName: givenName lastName: sn @@ -89,6 +110,7 @@ micronaut: base: "DC=domain,DC=local" filter: "(&(objectClass=group)(member={0}))" filter-attribute: dn + attribute: cn ``` Key points for Windows Active Directory: @@ -144,9 +166,51 @@ Get-ADGroupMember -Identity "CN=Auto,OU=Distro,OU=Groups,DC=kestra,DC=local" | S Replace the identity string with the DN of your target group. +### Group sync with SSO (GROUP_SYNC_ONLY) + +If your users already authenticate via SSO, Basic auth, or Passwordless, you can use LDAP solely to resolve group memberships without changing how users log in. Set `mode: GROUP_SYNC_ONLY` and configure the `groups` block. No `user-attributes` mapping is required. + +```yaml +micronaut: + security: + ldap: + default: + mode: GROUP_SYNC_ONLY + context: + server: "ldap://localhost:389" + manager-dn: "cn=admin,dc=kestra,dc=io" + manager-password: "LDAP_ADMIN_PASSWORD" + search: + base: "ou=users,dc=kestra,dc=io" + filter: "(mail={0})" + groups: + enabled: true + base: "ou=groups,dc=kestra,dc=io" + filter: "(member={0})" + attribute: cn +``` + +With this configuration: +- Users log in using their SSO provider. LDAP credentials are never checked. +- At each login, Kestra queries the LDAP directory for the user's group memberships and merges them with any groups sourced from OIDC claims. +- Groups found in LDAP are synced to Kestra using the same rules as standard LDAP group sync — new groups are created automatically, and membership is updated on login. + +Two `groups` properties control how Kestra reads group entries from the directory: +- `filter`: the LDAP search filter used to find groups for a user. `{0}` is replaced with the user's distinguished name (DN). +- `attribute`: the attribute on the group entry whose value becomes the Kestra group name. Defaults to `cn`. +- `filter-attribute`: the user entry attribute substituted into `{0}` in the group filter. Use `dn` for directories that store full DNs in group membership attributes (common in Active Directory). Use `uid` for POSIX-style directories. + +:::alert{type="info"} +`GROUP_SYNC_ONLY` mode requires that the user already exists in Kestra (created on first login). LDAP group sync fires on every subsequent login. +::: + +:::alert{type="warning"} +If the LDAP server is unreachable or misconfigured, group sync fails silently — the user logs in successfully but receives no LDAP-sourced groups. Check server connectivity and `groups` configuration if group assignments are not appearing after login. +::: + ## LDAP users in Kestra -Once LDAP is configured, when a user logs into Kestra for the first time, their credentials are validated against the LDAP directory, and a corresponding user is created in Kestra. If a matching account already exists in Kestra, the user is authenticated using their LDAP credentials. +Once LDAP is configured, when a user logs into Kestra for the first time using LDAP authentication, their credentials are validated against the LDAP directory and a corresponding user is created in Kestra. If a matching account already exists, the user is authenticated using their LDAP credentials. If they are a part of any groups specified in the directory, those groups will be added to Kestra. If the group already exists in Kestra, they will be automatically added. If a user is added to a group after their initial login, they must log out and log back in for the new group assignment to sync, as synchronization occurs only at login. Any user authenticated via LDAP will show `LDAP` as their Authentication method in the **IAM - Users** tab in Kestra. @@ -154,6 +218,10 @@ If they are a part of any groups specified in the directory, those groups will b Any updates to a user and their group access on the LDAP server will update in Kestra at the next synchronization (typically at the next login). +:::alert{type="info"} +Users who log in via SSO with `GROUP_SYNC_ONLY` mode show their SSO provider as their Authentication method in the IAM Users tab, not `LDAP`. The LDAP connection is used only to resolve group memberships in the background. +::: + :::alert{type="warning"} If a user is deleted from the LDAP server, they will lose access to Kestra at the next synchronization or login attempt. ::: From 84749d3536ee95291bda29db2fe7ae5db4ded6e4 Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Mon, 20 Apr 2026 15:12:54 +0200 Subject: [PATCH 20/52] docs(checks.conditions): conditions now when Part of https://github.com/kestra-io/kestra-ee/issues/7424 --- src/contents/blogs/release-1-2/index.md | 2 +- .../05.workflow-components/07.checks/index.md | 10 ++--- .../checks-condition-renamed-when/index.md | 43 +++++++++++++++++++ 3 files changed, 49 insertions(+), 6 deletions(-) create mode 100644 src/contents/docs/11.migration-guide/v2.0.0/checks-condition-renamed-when/index.md diff --git a/src/contents/blogs/release-1-2/index.md b/src/contents/blogs/release-1-2/index.md index c5804754e81..d388e22e441 100644 --- a/src/contents/blogs/release-1-2/index.md +++ b/src/contents/blogs/release-1-2/index.md @@ -360,7 +360,7 @@ id: vm_provisioning namespace: company.team checks: - - condition: "{{ kv('VMs') | length < 2 }}" + - when: "{{ kv('VMs') | length < 2 }}" message: "You have provisioned too many VMs" style: ERROR behavior: BLOCK_EXECUTION diff --git a/src/contents/docs/05.workflow-components/07.checks/index.md b/src/contents/docs/05.workflow-components/07.checks/index.md index 98102289bff..2ff7b7de805 100644 --- a/src/contents/docs/05.workflow-components/07.checks/index.md +++ b/src/contents/docs/05.workflow-components/07.checks/index.md @@ -11,7 +11,7 @@ Add pre-execution validations that can block or fail an execution before any tas ## Add checks to validate inputs before execution -`checks` are flow-level assertions evaluated when validating inputs and before creating a new execution. Each check defines a boolean `condition` and a `message` shown when the condition is false. You can choose how Kestra reacts (block, fail, or still create the execution) and how the message is styled in the UI. +`checks` are flow-level assertions evaluated when validating inputs and before creating a new execution. Each check defines a boolean `when` expression and a `message` shown when the expression evaluates to false. You can choose how Kestra reacts (block, fail, or still create the execution) and how the message is styled in the UI. Checks are useful to enforce business rules on inputs (e.g., allowed values, date windows, required flags) or to nudge users with warnings before they launch a run. @@ -19,7 +19,7 @@ Checks are useful to enforce business rules on inputs (e.g., allowed values, dat Each item in `checks` supports the following properties: -- `condition` *(required)*: Pebble expression that must evaluate to a boolean. For example, you can design checks against Inputs, Key-Value pairs, or other [expression](../../expressions/index.mdx) accessible workflow components. +- `when` *(required)*: Pebble expression that must evaluate to a boolean. For example, you can design checks against Inputs, Key-Value pairs, or other [expression](../../expressions/index.mdx) accessible workflow components. - `message` *(required)*: Text displayed when the condition is false. - `style` *(optional, default `INFO`)*: Visual style for the message. One of `ERROR`, `SUCCESS`, `WARNING`, `INFO`. - `behavior` *(optional, default `BLOCK_EXECUTION`)*: How the flow should react when the condition is false. One of: @@ -53,7 +53,7 @@ inputs: checks: - message: "Sorry, this flow can only be executed with 'Kestra'" - condition: "{{ (inputs.name | upper) == 'KESTRA' }}" + when: "{{ (inputs.name | upper) == 'KESTRA' }}" style: ERROR behavior: BLOCK_EXECUTION @@ -86,13 +86,13 @@ inputs: checks: # Block risky prod runs outside the allowed window - message: "Prod runs are only allowed between 06:00 and 22:00 UTC" - condition: "{{ inputs.environment != 'prod' or (inputs.run_date | date('HH') | number >= 6 and inputs.run_date | date('HH') | number < 22) }}" + when: "{{ inputs.environment != 'prod' or (inputs.run_date | date('HH') | number >= 6 and inputs.run_date | date('HH') | number < 22) }}" style: ERROR behavior: BLOCK_EXECUTION # Warn if the payload is not the approved source - message: "Non-approved source detected. Use https://dummyjson.com when possible." - condition: "{{ inputs.payload_url | startsWith('https://dummyjson.com') }}" + when: "{{ inputs.payload_url | startsWith('https://dummyjson.com') }}" style: WARNING behavior: CREATE_EXECUTION diff --git a/src/contents/docs/11.migration-guide/v2.0.0/checks-condition-renamed-when/index.md b/src/contents/docs/11.migration-guide/v2.0.0/checks-condition-renamed-when/index.md new file mode 100644 index 00000000000..22706c09647 --- /dev/null +++ b/src/contents/docs/11.migration-guide/v2.0.0/checks-condition-renamed-when/index.md @@ -0,0 +1,43 @@ +--- +title: Check.condition Renamed to when +sidebarTitle: condition → when (Checks) +icon: /src/contents/docs/icons/migration-guide.svg +release: 2.0.0 +editions: ["OSS", "EE"] +description: The condition property on flow-level checks has been renamed to when in Kestra 2.0.0, aligning it with the when property used across tasks and triggers. +--- + +Kestra 2.0.0 unifies all Pebble conditional expressions under a single property name: `when`. + +The `condition` property on flow-level `checks` is renamed to `when`. A deprecated alias keeps `condition` functional in 2.0.0 so existing flows continue to parse without changes. The alias is scheduled for removal in a future version — update your flows now to avoid a hard break later. + +## Before + +```yaml +checks: + - condition: "{{ inputs.environment == 'production' }}" + message: "This flow can only run in production" + behavior: BLOCK_EXECUTION + style: ERROR +``` + +## After + +```yaml +checks: + - when: "{{ inputs.environment == 'production' }}" + message: "This flow can only run in production" + behavior: BLOCK_EXECUTION + style: ERROR +``` + +The behavior is identical — the same Pebble rendering and `BLOCK_EXECUTION` / `FAIL_EXECUTION` / `CREATE_EXECUTION` logic apply. + +## Migration steps + +1. **Search your flows** for `condition:` inside `checks` blocks and replace each occurrence with `when:`. The property value and any Pebble expressions stay the same. +2. **Validate** by saving the updated flows in the Kestra UI or via the API. + +:::alert{type="warning"} +The `condition` alias will be removed in a future release. Flows that still use `condition` inside `checks` will fail to parse after the alias is dropped. +::: From f6959675d21eae18bcd85f91b70e9ff030f6b49e Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Mon, 20 Apr 2026 15:48:25 +0200 Subject: [PATCH 21/52] docs(trigger-redesign) Part of https://github.com/kestra-io/kestra-ee/issues/3033 --- .../07.triggers/02.flow-trigger/index.md | 369 ++++++++----- .../07.triggers/03.webhook-trigger/index.md | 36 ++ .../07.triggers/index.mdx | 61 ++- .../trigger-conditions-redesign/index.md | 499 ++++++++++++++++++ 4 files changed, 826 insertions(+), 139 deletions(-) create mode 100644 src/contents/docs/11.migration-guide/v2.0.0/trigger-conditions-redesign/index.md diff --git a/src/contents/docs/05.workflow-components/07.triggers/02.flow-trigger/index.md b/src/contents/docs/05.workflow-components/07.triggers/02.flow-trigger/index.md index f4743601e6a..fa301baf27a 100644 --- a/src/contents/docs/05.workflow-components/07.triggers/02.flow-trigger/index.md +++ b/src/contents/docs/05.workflow-components/07.triggers/02.flow-trigger/index.md @@ -20,23 +20,17 @@ Kestra can automatically start a flow as soon as another flow ends. This allows Check the [Flow trigger](/plugins/core/trigger/io.kestra.plugin.core.trigger.flow) documentation for the list of all properties. -## Preconditions +## Upstream flow dependencies -A Flow trigger requires preconditions to filter which upstream executions can trigger the flow, often within a defined time window. +The `dependsOn` property is a list of upstream flow entries that must all complete in matching states before the trigger fires. -:::alert{type="info"} -[Pebble expressions](../../../expressions/index.mdx) cannot be used in Flow Trigger (pre)conditions. You must declaratively define any condition variables. -::: - -### Filters - -- `flows`: A list of preconditions to meet, in the form of upstream flows +### Basic single upstream flow -The example below shows a Flow trigger that runs when `flow_a` completes successfully. +The example below triggers `flow_b` when `flow_a` from the `company.team` namespace completes successfully: ```yaml id: flow_b -namespace: kestra.sandbox +namespace: company.team tasks: - id: hello @@ -44,191 +38,320 @@ tasks: message: "Hello World!" triggers: - - id: upstream_dependancy + - id: after_extract type: io.kestra.plugin.core.trigger.Flow - preconditions: - id: flow_trigger - flows: - - namespace: kestra.sandbox - flowId: flow_a - states: [SUCCESS] + dependsOn: + - flowId: extract + namespace: company.team + states: [SUCCESS] ``` -:::alert{type="info"} -It is [best practice](../../../14.best-practices/0.flows/index.md#flow-trigger-on-state-change) when using a flow trigger to use `preconditions.flows.states` rather than the `states` task property when defining state conditions for one specific flow. -::: +### Multiple upstream flows + +List multiple entries under `dependsOn`. All entries must be satisfied before the trigger fires: + +```yaml +triggers: + - id: after_staging + type: io.kestra.plugin.core.trigger.Flow + dependsOn: + - flowId: stg_sales + namespace: company.team + - flowId: stg_marketing + namespace: company.team +``` + +### Entry properties + +| Property | Type | Description | +|-------------|-----------------------|----------------------------------------------------------------------------------------------------------------------| +| `flowId` | `String` | The ID of the upstream flow to match. Omit to match any flow (combine with `when` to narrow the scope). | +| `namespace` | `String` | The namespace of the upstream flow. Exact match only — use `when` for prefix or pattern matching. | +| `states` | `List` | States that satisfy this entry. Defaults to `[SUCCESS, WARNING]`. | +| `labels` | `Map` | Key-value pairs that must all be present on the upstream execution's labels. | +| `when` | `String` | A Pebble expression evaluated against the upstream execution. The entry is satisfied only when this evaluates to true.| -- `where`: filter executions based on fields like `FLOW_ID`, `NAMESPACE`, `STATE`, and `EXPRESSION`. +### Prefix and pattern matching -For example, the following Flow Trigger triggers on execution from flows in FAILED or WARNING states in namespaces starting with "company": +When `namespace` is set, Kestra matches it exactly. To match a range of namespaces or flows, omit `namespace` and use `when` with a Pebble expression: ```yaml triggers: - id: alert_on_failure type: io.kestra.plugin.core.trigger.Flow - states: - - FAILED - - WARNING - preconditions: - id: company_namespace - where: - - id: company - filters: - - field: NAMESPACE - type: STARTS_WITH - value: company + dependsOn: + - states: [FAILED, WARNING] + when: "{{ namespace | startsWith('company') }}" ``` -### Time Window & SLA +## Conditional guard with `when` -The `timeWindow` property lets you define how Kestra evaluates upstream flow executions over time. It supports several modes: +Like all triggers, the Flow trigger supports a top-level `when` Pebble expression. It is evaluated before `dependsOn` — if it returns a falsy value, the trigger does not fire regardless of upstream state: -- `DURATION_WINDOW`: This is the default type. It uses a start time (windowAdvance) and end time (window) that are moving forward to the next interval whenever the evaluation time reaches the end time, based on the defined duration window. +```yaml +triggers: + - id: after_extract + type: io.kestra.plugin.core.trigger.Flow + when: "{{ not isWeekend(trigger.date) }}" + dependsOn: + - flowId: extract + namespace: company.team +``` -For example, with a 1-day window (`window: PT1D`, the default), SLA conditions are evaluated over a 24-hour period starting at midnight each day. If you set `windowAdvance: PT6H`, the window will start at 6 AM each day. If you set `windowAdvance: PT6H` and you also override `window: PT6H`, the window will start at 6 AM and last for 6 hours — as a result, Kestra will check the SLA conditions during the following time periods: `06:00` to `12:00`, `12:00` to `18:00`, `18:00` to `00:00`, and `00:00` to `06:00`, and so on. +## Time window -- `SLIDING_WINDOW`: This option also evaluates SLA conditions over a fixed time window, but it always goes backward from the current time. For example, a sliding window of 1 hour (window: PT1H) will evaluate executions for the past hour (so between now and one hour before now). It uses a default window of 1 day. +The `window` property controls how long Kestra accumulates upstream executions before evaluating whether all `dependsOn` entries are satisfied. -For example, the flow below evaluates every hour if the flow `flow_a` is in SUCCESS state. If so, it triggers the `flow_b` passing corresponding inputs (reading `flow_a` outputs). +### Deadline + +All upstream flows must complete before a fixed time each day. The deadline string must include a timezone offset: ```yaml -id: flow_b -namespace: kestra.sandbox +triggers: + - id: after_staging + type: io.kestra.plugin.core.trigger.Flow + dependsOn: + - flowId: stg_sales + namespace: company.team + - flowId: stg_marketing + namespace: company.team + window: + deadline: "09:00:00+01:00" +``` -inputs: - - id: value_from_a - type: STRING +### Daily time range -tasks: - - id: hello - type: io.kestra.plugin.core.log.Log - message: "{{ inputs.value_from_a }}" +Only executions that completed within a specific time range each day are counted: +```yaml triggers: - - id: upstream_dep + - id: after_staging type: io.kestra.plugin.core.trigger.Flow - inputs: - value_from_a: "{{ trigger.outputs.return_value }}" - preconditions: - id: test - flows: - - namespace: kestra.sandbox - flowId: flow_a - states: [SUCCESS] - timeWindow: - type: SLIDING_WINDOW - window: PT1H + dependsOn: + - flowId: stg_sales + namespace: company.team + - flowId: stg_marketing + namespace: company.team + window: + from: "06:00:00" + to: "12:00:00" ``` -For reference, below is `flow_a`: +### Fixed interval + +`every` defines the window size and `offset` shifts its start relative to midnight: ```yaml -id: flow_a -namespace: kestra.sandbox +triggers: + - id: after_staging + type: io.kestra.plugin.core.trigger.Flow + dependsOn: + - flowId: stg_sales + namespace: company.team + window: + every: PT1H + offset: PT30M +``` -tasks: - - id: hello - type: io.kestra.plugin.core.log.Log - message: Hello World! 🚀 +### Lookback -outputs: - - id: return_value - type: STRING - value: "Flow A run succesfully" +Count executions that completed within the past duration, relative to the current evaluation time: + +```yaml +triggers: + - id: after_staging + type: io.kestra.plugin.core.trigger.Flow + dependsOn: + - flowId: stg_sales + namespace: company.team + window: + lookback: PT1H +``` + +### Fire once per window + +Set `fireOnce: true` to ensure the trigger fires at most once per window, even if conditions are satisfied multiple times: + +```yaml +window: + deadline: "09:00:00+01:00" + fireOnce: true ``` +## Scoped trigger outputs + +When a Flow trigger fires, upstream execution outputs are available under `trigger.outputs`. In Kestra 2.0, outputs are scoped by namespace and flow ID to avoid key collisions when multiple upstream flows are involved: -- `DAILY_TIME_DEADLINE`: This option enforces SLA conditions that must be met before a specific cutoff time each day. With the string property deadline, you can configure a daily cutoff for checking conditions. For example, deadline: `09:00:00.00Z` means that the defined SLA conditions should be met from midnight until 9 AM each day; otherwise, the flow will not be triggered. +``` +trigger.outputs... +``` -For the example, this trigger definition only triggers the flow if `flow_a` is in SUCCESS state before `9:00` AM every day. +For example, to pass an output from an upstream flow named `extract` in the `company.team` namespace: ```yaml triggers: - - id: upstream_dep + - id: after_extract + type: io.kestra.plugin.core.trigger.Flow + inputs: + date: "{{ trigger.outputs.company.team.extract.date }}" + dependsOn: + - flowId: extract + namespace: company.team +``` + +:::alert{type="warning"} +The output scoping format changed in Kestra 2.0. If you previously used `trigger.outputs.` (a flat map), update your expressions to the new `trigger.outputs...` format. +::: + +## Label-based filtering + +Use the `labels` map on a `dependsOn` entry to restrict which upstream executions are counted. All specified labels must be present on the upstream execution: + +```yaml +triggers: + - id: after_prod type: io.kestra.plugin.core.trigger.Flow - preconditions: - id: should_be_success_by_nine - flows: - - namespace: kestra.sandbox - flowId: flow_a - states: [SUCCESS] - timeWindow: - type: DAILY_TIME_DEADLINE - deadline: "09:00:00.00Z" + dependsOn: + - namespace: company.team + labels: + env: production + states: [SUCCESS] ``` -- `DAILY_TIME_WINDOW`: This option enforces SLA conditions that must be met within a specific daily time range. For example, a window from `startTime: "06:00:00"` to `endTime: "09:00:00"` evaluates executions within that interval each day. This option is particularly useful for declarative definition of freshness conditions when building data pipelines. For example, if you only need one successful execution within a given time range to guarantee that some data has been successfully refreshed in order for you to proceed with the next steps of your pipeline, this option can be more useful than a strict DAG-based approach. Usually, each failure in your flow would block the entire pipeline, whereas with this option, you can proceed with the next steps of the pipeline as soon as the data is successfully refreshed at least once within the given time range. +## Filtering with `when` expressions + +Use `when` on a `dependsOn` entry to apply arbitrary Pebble conditions against the upstream execution context. + +Filter on an output value: ```yaml triggers: - - id: upstream_dep + - id: after_extract type: io.kestra.plugin.core.trigger.Flow - inputs: - value_from_a: "{{ trigger.outputs.return_value }}" - preconditions: - id: test - flows: - - namespace: kestra.sandbox - flowId: flow_a - states: [SUCCESS] - timeWindow: - type: DAILY_TIME_WINDOW - startTime: "06:00:00" - endTime: "12:00:00" + dependsOn: + - flowId: extract + namespace: company.team + when: "{{ outputs.row_count > 0 }}" ``` +Filter on retry attempts: +```yaml +triggers: + - id: after_flaky + type: io.kestra.plugin.core.trigger.Flow + dependsOn: + - flowId: flaky_pipeline + namespace: company.team + states: [SUCCESS] + when: "{{ hasRetryAttempt == true }}" +``` -## Example +## Example: data pipeline with SLA deadline -This example triggers the `silver_layer` flow once the `bronze_layer` flow finishes successfully by 9 AM. The deadline time string must include the timezone offset. This ensures that no new executions are triggered past the deadline. Here is the `silver_layer` flow: +This example triggers the `silver_layer` flow once the `bronze_layer` flow finishes successfully by 9 AM: ```yaml id: silver_layer namespace: company.team + tasks: - id: transform_data type: io.kestra.plugin.core.log.Log message: deduplication, cleaning, and minor aggregations + triggers: - id: flow_trigger type: io.kestra.plugin.core.trigger.Flow - preconditions: - id: bronze_layer - timeWindow: - type: DAILY_TIME_DEADLINE - deadline: "09:00:00+01:00" - flows: - - namespace: company.team - flowId: bronze_layer - states: [SUCCESS] + dependsOn: + - flowId: bronze_layer + namespace: company.team + states: [SUCCESS] + window: + deadline: "09:00:00+01:00" ``` -## Example: Alerting +## Example: alerting on failure -This example creates a `System Flow` to send a Slack alert on any failure or warning state within the `company` namespace. This example uses the Slack webhook secret to notify the `#general` channel about the failed flow. +This example creates a system flow that sends a Slack alert on any failure or warning state within the `company` namespace: ```yaml id: alert namespace: system + tasks: - id: send_alert type: io.kestra.plugin.slack.notifications.SlackExecution - url: "{{secret('SLACK_WEBHOOK')}}" # format: https://hooks.slack.com/services/xzy/xyz/xyz + url: "{{secret('SLACK_WEBHOOK')}}" channel: "#general" executionId: "{{trigger.executionId}}" + triggers: - id: alert_on_failure type: io.kestra.plugin.core.trigger.Flow - states: - - FAILED - - WARNING - preconditions: - id: company_namespace - where: - - id: company - filters: - - field: NAMESPACE - type: STARTS_WITH - value: company + dependsOn: + - states: [FAILED, WARNING] + when: "{{ namespace | startsWith('company') }}" +``` + +## Example: mixed success and failure triggers + +You can define multiple Flow triggers on the same flow to react differently to upstream success vs. failure: + +```yaml +triggers: + - id: on_completion + type: io.kestra.plugin.core.trigger.Flow + dependsOn: + - flowId: flow_a + namespace: company.team + states: [SUCCESS] + - id: on_failure + type: io.kestra.plugin.core.trigger.Flow + dependsOn: + - flowId: flow_a + namespace: company.team + states: [FAILED] ``` + +## Example: passing upstream outputs downstream + +Reference upstream outputs using the scoped path `trigger.outputs...`: + +```yaml +id: flow_b +namespace: company.team + +inputs: + - id: value_from_a + type: STRING + +tasks: + - id: hello + type: io.kestra.plugin.core.log.Log + message: "{{ inputs.value_from_a }}" + +triggers: + - id: upstream_dep + type: io.kestra.plugin.core.trigger.Flow + inputs: + value_from_a: "{{ trigger.outputs.kestra.sandbox.flow_a.return_value }}" + dependsOn: + - flowId: flow_a + namespace: kestra.sandbox + states: [SUCCESS] +``` + +:::alert{type="info"} +`dependsOn` condition IDs are derived from a stable hash of each entry's `namespace`, `flowId`, `when`, `states`, and `labels`. Reordering entries in the list does not reset accumulated window state. +::: + +## Input rendering failures create FAILED executions + +If an `inputs` expression on a Flow trigger fails to render — for example, because an upstream output key does not exist — Kestra creates a `FAILED` execution instead of silently dropping the event. This makes failures visible in the UI and actionable via alerting. + +## Deprecated: `preconditions` and `conditions` + +The `preconditions` and `conditions` properties are still functional in Kestra 2.0 but are deprecated and will be removed in a future release. Migrate to `dependsOn` at your earliest convenience. + +See the [trigger conditions migration guide](../../../11.migration-guide/v2.0.0/trigger-conditions-redesign/index.md) for a complete before/after reference. diff --git a/src/contents/docs/05.workflow-components/07.triggers/03.webhook-trigger/index.md b/src/contents/docs/05.workflow-components/07.triggers/03.webhook-trigger/index.md index ef7f952b986..9f1176e08d1 100644 --- a/src/contents/docs/05.workflow-components/07.triggers/03.webhook-trigger/index.md +++ b/src/contents/docs/05.workflow-components/07.triggers/03.webhook-trigger/index.md @@ -88,6 +88,42 @@ If your flow uses trigger variables (such as `{{ trigger.body }})`, you can test See the [Webhook trigger plugin documentation](/plugins/core/trigger/io.kestra.plugin.core.trigger.webhook) for a full list of properties and outputs. +## Filtering webhook executions with `when` + +Use the `when` property to conditionally fire the trigger based on the request body or headers. The `when` value is a [Pebble expression](../../../expressions/index.mdx) evaluated against the incoming request. If the expression evaluates to a falsy value, Kestra ignores the request and no execution is created. + +### Before + +```yaml +triggers: + - id: webhook + type: io.kestra.plugin.core.trigger.Webhook + key: 4wjtkzwVGBM9yKnjm3yv8r + conditions: + - type: io.kestra.plugin.core.condition.Expression + expression: "{{ trigger.body.hello == 'world' }}" +``` + +### After + +```yaml +triggers: + - id: webhook + type: io.kestra.plugin.core.trigger.Webhook + key: 4wjtkzwVGBM9yKnjm3yv8r + when: "{{ trigger.body.hello == 'world' }}" +``` + +You can combine multiple criteria in a single expression using `and` / `or`: + +```yaml +triggers: + - id: webhook + type: io.kestra.plugin.core.trigger.Webhook + key: 4wjtkzwVGBM9yKnjm3yv8r + when: "{{ trigger.body.event == 'push' and trigger.headers['x-github-event'] == 'push' }}" +``` + ### Return flow outputs in the webhook response To send task outputs back to the caller in the HTTP response, configure the Webhook trigger to wait for the execution and return outputs. The flow must expose at least one `outputs` entry. diff --git a/src/contents/docs/05.workflow-components/07.triggers/index.mdx b/src/contents/docs/05.workflow-components/07.triggers/index.mdx index a930b12908d..1e7b58a9aed 100644 --- a/src/contents/docs/05.workflow-components/07.triggers/index.mdx +++ b/src/contents/docs/05.workflow-components/07.triggers/index.mdx @@ -53,6 +53,7 @@ The following properties are common to all triggers: | `disabled` | Set it to `true` to disable execution of the trigger. | | `allowConcurrent` | Set it to `true` to allow multiple executions from this trigger to run at the same time. | | `workerGroup.key` | To execute this trigger on a specific Worker Group (EE). | +| `when` | A Pebble expression that must evaluate to `true` for the trigger to fire. | --- @@ -103,28 +104,56 @@ triggers: cron: "@hourly" ``` -## Conditions +## `when` and `dependsOn` -Conditions are criteria that determine when a trigger should create a new execution. Usually, they limit the scope of a trigger to a specific set of cases. +Kestra 2.0 replaces the old `conditions` list on triggers with two composable properties: -For example, you can restrict a Flow trigger to a specific namespace prefix or execution status, and you can restrict a Schedule trigger to a specific time of the week or month. +### `when` — Pebble expression (all triggers) -You can pass a list of conditions; in this case, all the conditions must match to enable the current action. +Every trigger type supports a `when` property. It accepts a [Pebble expression](../../expressions/index.mdx) that is evaluated at trigger time. If the expression evaluates to a falsy value (`false`, `0`, empty string), the trigger does not fire. -Available conditions include: +Use `when` to express time-based conditions on Schedule triggers, to filter Webhook payloads, or to add a global guard on any trigger type: -- [HasRetryAttempt](/plugins/core/condition/io.kestra.plugin.core.condition.hasretryattempt) -- [MultipleCondition](/plugins/core) -- [Not](/plugins/core/condition/io.kestra.plugin.core.condition.not) -- [Or](/plugins/core/condition/io.kestra.plugin.core.condition.or) -- [ExecutionFlow](/plugins/core/condition/io.kestra.plugin.core.condition.executionflow) -- [ExecutionNamespace](/plugins/core/condition/io.kestra.plugin.core.condition.executionnamespace) -- [ExecutionLabels](/plugins/core/condition/io.kestra.plugin.core.condition.executionlabels) -- [ExecutionStatus](/plugins/core/condition/io.kestra.plugin.core.condition.executionstatus) -- [ExecutionOutputs](/plugins/core/condition/io.kestra.plugin.core.condition.executionoutputs) -- [Expression](/plugins/core/condition/io.kestra.plugin.core.condition.expression) +```yaml +triggers: + - id: schedule + type: io.kestra.plugin.core.trigger.Schedule + cron: "0 9 * * *" + when: "{{ not isWeekend(trigger.date) and not isPublicHoliday(trigger.date, 'FR') }}" +``` + +```yaml +triggers: + - id: webhook + type: io.kestra.plugin.core.trigger.Webhook + key: 4wjtkzwVGBM9yKnjm3yv8r + when: "{{ trigger.body.hello == 'world' }}" +``` + +For calendar-based scheduling, helper functions like `isWeekend()`, `isPublicHoliday()`, `isDayWeekInMonth()`, and `dayOfWeek()` are available. See [date and calendar helpers](../../expressions/index.mdx#date-and-calendar-helpers) in the expressions reference. + +### `dependsOn` — upstream flow dependencies (Flow trigger only) -You can also find datetime related conditions [on the Schedule trigger page](./01.schedule-trigger/index.md#schedule-conditions). +The [Flow trigger](./02.flow-trigger/index.md) replaces both `conditions` and `preconditions` with a `dependsOn` list. Each entry declares one upstream flow that must complete in a matching state. All entries must be satisfied before the trigger fires. + +```yaml +triggers: + - id: after_staging + type: io.kestra.plugin.core.trigger.Flow + dependsOn: + - flowId: stg_sales + namespace: company.team + - flowId: stg_marketing + namespace: company.team + window: + deadline: "09:00:00+01:00" +``` + +See the [Flow trigger documentation](./02.flow-trigger/index.md) for the full `dependsOn` and `window` property reference. + +:::alert{type="info"} +The old `conditions` list is deprecated in Kestra 2.0 but still functional. Migrate to `when` at your earliest convenience. See the [trigger conditions migration guide](../../11.migration-guide/v2.0.0/trigger-conditions-redesign/index.md) for before/after examples. +::: ## Unlocking, enabling, and disabling triggers diff --git a/src/contents/docs/11.migration-guide/v2.0.0/trigger-conditions-redesign/index.md b/src/contents/docs/11.migration-guide/v2.0.0/trigger-conditions-redesign/index.md new file mode 100644 index 00000000000..f6ba918ff30 --- /dev/null +++ b/src/contents/docs/11.migration-guide/v2.0.0/trigger-conditions-redesign/index.md @@ -0,0 +1,499 @@ +--- +title: Trigger Conditions Redesign +sidebarTitle: Trigger Conditions Redesign +icon: /src/contents/docs/icons/migration-guide.svg +release: 2.0.0 +editions: ["OSS", "EE"] +description: The conditions list on triggers is deprecated in Kestra 2.0 in favor of a when Pebble expression. The preconditions property on Flow triggers is replaced by dependsOn and window. +--- + +Kestra 2.0 simplifies and unifies how trigger conditions are expressed. + +- The `conditions` list on all triggers is **deprecated** in favor of a single `when` Pebble expression string. +- The `preconditions` property on Flow triggers is **deprecated** in favor of `dependsOn` (for upstream flow entries) and `window` (for time windows). +- Flow trigger outputs are now **scoped** by namespace and flow ID: `trigger.outputs...`. +- Input rendering failures on Flow triggers now create a **`FAILED` execution** instead of silently dropping the event. + +All deprecated properties remain functional in 2.0.0. Migrate at your earliest convenience to avoid a hard break when they are removed in a future release. + +## `conditions` → `when` on all trigger types + +### Schedule trigger: specific day of week + +**Before** + +```yaml +triggers: + - id: schedule + type: io.kestra.plugin.core.trigger.Schedule + cron: "0 9 * * *" + conditions: + - type: io.kestra.plugin.core.condition.DayWeek + dayOfWeek: MONDAY +``` + +**After** + +```yaml +triggers: + - id: schedule + type: io.kestra.plugin.core.trigger.Schedule + cron: "0 9 * * *" + when: "{{ dayOfWeek(trigger.date) == 'MONDAY' }}" +``` + +### Schedule trigger: weekends only + +**Before** + +```yaml +triggers: + - id: schedule + type: io.kestra.plugin.core.trigger.Schedule + cron: "0 11 * * *" + conditions: + - type: io.kestra.plugin.core.condition.Weekend +``` + +**After** + +```yaml +triggers: + - id: schedule + type: io.kestra.plugin.core.trigger.Schedule + cron: "0 11 * * *" + when: "{{ isWeekend(trigger.date) }}" +``` + +### Schedule trigger: public holidays + +**Before** + +```yaml +triggers: + - id: schedule + type: io.kestra.plugin.core.trigger.Schedule + cron: "0 11 * * *" + conditions: + - type: io.kestra.plugin.core.condition.PublicHoliday + country: FR +``` + +**After** + +```yaml +triggers: + - id: schedule + type: io.kestra.plugin.core.trigger.Schedule + cron: "0 11 * * *" + when: "{{ isPublicHoliday(trigger.date, 'FR') }}" +``` + +### Schedule trigger: workdays only (not weekend, not public holiday) + +**Before** + +```yaml +triggers: + - id: schedule + type: io.kestra.plugin.core.trigger.Schedule + cron: "0 11 * * *" + conditions: + - type: io.kestra.plugin.core.condition.Not + conditions: + - type: io.kestra.plugin.core.condition.PublicHoliday + country: FR + - type: io.kestra.plugin.core.condition.Weekend +``` + +**After** + +```yaml +triggers: + - id: schedule + type: io.kestra.plugin.core.trigger.Schedule + cron: "0 11 * * *" + when: "{{ not isWeekend(trigger.date) and not isPublicHoliday(trigger.date, 'FR') }}" +``` + +### Schedule trigger: first Monday of the month + +**Before** + +```yaml +triggers: + - id: schedule + type: io.kestra.plugin.core.trigger.Schedule + cron: "0 11 * * 1" + conditions: + - type: io.kestra.plugin.core.condition.DayWeekInMonth + dayOfWeek: MONDAY + dayInMonth: FIRST +``` + +**After** + +```yaml +triggers: + - id: schedule + type: io.kestra.plugin.core.trigger.Schedule + cron: "0 11 * * 1" + when: "{{ isDayWeekInMonth(trigger.date, 'MONDAY', 'FIRST') }}" +``` + +### Schedule trigger: date range + +**Before** + +```yaml +triggers: + - id: schedule + type: io.kestra.plugin.core.trigger.Schedule + cron: "*/5 * * * *" + conditions: + - type: io.kestra.plugin.core.condition.DateTimeBetween + after: "2025-12-31T23:59:59Z" + before: "2026-06-30T23:59:59Z" +``` + +**After** + +```yaml +triggers: + - id: schedule + type: io.kestra.plugin.core.trigger.Schedule + cron: "*/5 * * * *" + when: "{{ trigger.date > '2025-12-31T23:59:59Z' and trigger.date < '2026-06-30T23:59:59Z' }}" +``` + +### Webhook trigger: filter by body or headers + +**Before** + +```yaml +triggers: + - id: webhook + type: io.kestra.plugin.core.trigger.Webhook + key: 4wjtkzwVGBM9yKnjm3yv8r + conditions: + - type: io.kestra.plugin.core.condition.Expression + expression: "{{ trigger.body.hello == 'world' }}" +``` + +**After** + +```yaml +triggers: + - id: webhook + type: io.kestra.plugin.core.trigger.Webhook + key: 4wjtkzwVGBM9yKnjm3yv8r + when: "{{ trigger.body.hello == 'world' }}" +``` + +For the full list of Pebble calendar helper functions (`isWeekend`, `isPublicHoliday`, `isDayWeekInMonth`, etc.), see the [date and calendar helpers](../../../expressions/index.mdx#date-and-calendar-helpers) reference. + +## `preconditions` → `dependsOn` on Flow triggers + +The `preconditions` block is replaced by two top-level properties on the Flow trigger: + +- `dependsOn` — a list of upstream flow entries that must all be satisfied. +- `window` — controls the time window over which upstream executions are accumulated. + +### Single upstream flow + +**Before** + +```yaml +triggers: + - id: after_extract + type: io.kestra.plugin.core.trigger.Flow + preconditions: + id: flows + flows: + - namespace: company.team + flowId: extract + states: [SUCCESS] +``` + +**After** + +```yaml +triggers: + - id: after_extract + type: io.kestra.plugin.core.trigger.Flow + dependsOn: + - flowId: extract + namespace: company.team + states: [SUCCESS] +``` + +### Multiple upstream flows with a deadline + +**Before** + +```yaml +triggers: + - id: after_staging + type: io.kestra.plugin.core.trigger.Flow + preconditions: + id: staging_deps + timeWindow: + type: DAILY_TIME_DEADLINE + deadline: "09:00:00+01:00" + flows: + - namespace: company.team + flowId: stg_sales + states: [SUCCESS] + - namespace: company.team + flowId: stg_marketing + states: [SUCCESS] +``` + +**After** + +```yaml +triggers: + - id: after_staging + type: io.kestra.plugin.core.trigger.Flow + dependsOn: + - flowId: stg_sales + namespace: company.team + - flowId: stg_marketing + namespace: company.team + window: + deadline: "09:00:00+01:00" +``` + +The default `states` for a `dependsOn` entry is `[SUCCESS, WARNING]`. Specify `states` explicitly when you need a different set. + +### Namespace-wide alerting (prefix matching) + +**Before** + +```yaml +triggers: + - id: alert_on_failure + type: io.kestra.plugin.core.trigger.Flow + conditions: + - type: io.kestra.plugin.core.condition.ExecutionStatus + in: + - FAILED + - WARNING + - type: io.kestra.plugin.core.condition.ExecutionNamespace + namespace: company + comparison: PREFIX +``` + +**After** + +```yaml +triggers: + - id: alert_on_failure + type: io.kestra.plugin.core.trigger.Flow + dependsOn: + - states: [FAILED, WARNING] + when: "{{ namespace | startsWith('company') }}" +``` + +### Label-based filtering + +**Before** + +```yaml +triggers: + - id: after_prod + type: io.kestra.plugin.core.trigger.Flow + conditions: + - type: io.kestra.plugin.core.condition.ExecutionStatus + in: [SUCCESS] + - type: io.kestra.plugin.core.condition.ExecutionLabels + labels: + env: production +``` + +**After** + +```yaml +triggers: + - id: after_prod + type: io.kestra.plugin.core.trigger.Flow + dependsOn: + - namespace: company.team + labels: + env: production + states: [SUCCESS] +``` + +### Conditional filtering with expressions + +**Before** + +```yaml +preconditions: + id: my_filter + where: + - id: flow1 + filters: + - field: NAMESPACE + type: STARTS_WITH + value: io.kestra.tests + - field: EXPRESSION + type: IS_TRUE + value: "{{ labels.some == 'label' }}" +``` + +**After** + +```yaml +dependsOn: + - when: "{{ namespace | startsWith('io.kestra.tests') }}" + states: [SUCCESS] + labels: + some: label +``` + +### Filtering on upstream execution outputs + +**Before** + +```yaml +conditions: + - type: io.kestra.plugin.core.condition.ExecutionOutputs + expression: "{{ outputs.row_count > 0 }}" +``` + +**After** + +```yaml +dependsOn: + - flowId: extract + namespace: company.team + when: "{{ outputs.row_count > 0 }}" +``` + +### Filtering on retry attempts + +**Before** + +```yaml +triggers: + - id: after_flaky + type: io.kestra.plugin.core.trigger.Flow + conditions: + - type: io.kestra.plugin.core.condition.HasRetryAttempt +``` + +**After** + +```yaml +triggers: + - id: after_flaky + type: io.kestra.plugin.core.trigger.Flow + dependsOn: + - flowId: flaky_pipeline + namespace: company.team + states: [SUCCESS] + when: "{{ hasRetryAttempt == true }}" +``` + +### Mixed triggers: success and failure on the same upstream flow + +**Before** + +```yaml +triggers: + - id: on_completion + type: io.kestra.plugin.core.trigger.Flow + states: [SUCCESS] + conditions: + - type: io.kestra.plugin.core.condition.ExecutionFlow + namespace: company.team + flowId: flow_a + - id: on_failure + type: io.kestra.plugin.core.trigger.Flow + states: [FAILED] + preconditions: + id: flowsFailure + flows: + - namespace: company.team + flowId: flow_a + states: [FAILED] +``` + +**After** + +```yaml +triggers: + - id: on_completion + type: io.kestra.plugin.core.trigger.Flow + dependsOn: + - flowId: flow_a + namespace: company.team + states: [SUCCESS] + - id: on_failure + type: io.kestra.plugin.core.trigger.Flow + dependsOn: + - flowId: flow_a + namespace: company.team + states: [FAILED] +``` + +## Scoped trigger outputs + +In Kestra 2.0, Flow trigger outputs are scoped by namespace and flow ID to prevent key collisions when multiple upstream flows are involved. + +**Before** (flat map, all upstream outputs merged together) + +```yaml +triggers: + - id: after_extract + type: io.kestra.plugin.core.trigger.Flow + inputs: + date: "{{ trigger.outputs.date }}" + preconditions: + id: flows + flows: + - namespace: company.team + flowId: extract + states: [SUCCESS] +``` + +**After** (scoped by namespace, then flow ID) + +```yaml +triggers: + - id: after_extract + type: io.kestra.plugin.core.trigger.Flow + inputs: + date: "{{ trigger.outputs.company.team.extract.date }}" + dependsOn: + - flowId: extract + namespace: company.team +``` + +The new path format is: `trigger.outputs...` + +:::alert{type="warning"} +This is a breaking change for any flow that reads from `trigger.outputs` without a namespace and flow ID prefix. Update all such expressions when migrating to Kestra 2.0. +::: + +## Silent failures → FAILED executions + +Previously, if the `inputs` expression on a Flow trigger failed to render (for example, because an upstream output key did not exist), the trigger silently dropped the event and no execution was created. In Kestra 2.0, a `FAILED` execution is created instead. This makes failures visible in the Kestra UI and allows you to configure downstream alerting. + +No migration action is required. Review your Flow trigger `inputs` expressions and ensure they reference valid output keys to avoid unexpected `FAILED` executions after upgrading. + +## Stable condition IDs + +`dependsOn` condition IDs are now derived from a stable hash of each entry's `namespace`, `flowId`, `when`, `states`, and `labels`. Previously, condition IDs were auto-incremented (`condition_1`, `condition_2`, …) and reordering entries would reset the accumulated window state. No action is required. + +## Migration steps + +1. **Search your flows for `conditions:` on trigger blocks** and replace each with an equivalent `when:` Pebble expression. Use the before/after examples above as a reference. +2. **Search your flows for `preconditions:` on Flow triggers** and replace with `dependsOn:` and (if applicable) `window:`. +3. **Update `trigger.outputs` references** from `trigger.outputs.` to `trigger.outputs...`. +4. **Validate** by saving the updated flows in the Kestra UI or via the API and confirming that they parse without warnings. + +:::alert{type="warning"} +Both `conditions` and `preconditions` will be removed in a future release. Flows that still use them will fail to parse after the aliases are dropped. +::: From 4c5b9cea1635afbc494ec07e02b3c8d8a23b2de0 Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Mon, 20 Apr 2026 15:56:32 +0200 Subject: [PATCH 22/52] docs(trigger-redesign): Enforce better style Part of https://github.com/kestra-io/kestra-ee/issues/3033 --- .../07.triggers/02.flow-trigger/index.md | 8 ++---- .../07.triggers/03.webhook-trigger/index.md | 24 ++--------------- .../07.triggers/index.mdx | 26 +++++++------------ 3 files changed, 14 insertions(+), 44 deletions(-) diff --git a/src/contents/docs/05.workflow-components/07.triggers/02.flow-trigger/index.md b/src/contents/docs/05.workflow-components/07.triggers/02.flow-trigger/index.md index fa301baf27a..e1e8cbfbbfd 100644 --- a/src/contents/docs/05.workflow-components/07.triggers/02.flow-trigger/index.md +++ b/src/contents/docs/05.workflow-components/07.triggers/02.flow-trigger/index.md @@ -8,15 +8,11 @@ icon: /src/contents/docs/icons/flow.svg Trigger one flow based on the execution of another flow. -## Flow trigger – chain flow executions - -A Flow trigger runs a flow after another flow completes, enabling event-driven workflows and dependencies across teams. - ```yaml type: io.kestra.plugin.core.trigger.Flow ``` -Kestra can automatically start a flow as soon as another flow ends. This allows you to create dependencies between flows, even when those flows are owned by different teams. +A Flow trigger runs a flow after another flow completes, enabling event-driven workflows and dependencies across teams. This allows you to create dependencies between flows, even when those flows are owned by different teams. Check the [Flow trigger](/plugins/core/trigger/io.kestra.plugin.core.trigger.flow) documentation for the list of all properties. @@ -180,7 +176,7 @@ window: ## Scoped trigger outputs -When a Flow trigger fires, upstream execution outputs are available under `trigger.outputs`. In Kestra 2.0, outputs are scoped by namespace and flow ID to avoid key collisions when multiple upstream flows are involved: +When a Flow trigger fires, upstream execution outputs are available under `trigger.outputs`. Outputs are scoped by namespace and flow ID to avoid key collisions when multiple upstream flows are involved: ``` trigger.outputs... diff --git a/src/contents/docs/05.workflow-components/07.triggers/03.webhook-trigger/index.md b/src/contents/docs/05.workflow-components/07.triggers/03.webhook-trigger/index.md index 9f1176e08d1..759536e404d 100644 --- a/src/contents/docs/05.workflow-components/07.triggers/03.webhook-trigger/index.md +++ b/src/contents/docs/05.workflow-components/07.triggers/03.webhook-trigger/index.md @@ -8,7 +8,7 @@ icon: /src/contents/docs/icons/flow.svg Trigger flows automatically in response to web-based events. -## Webhook trigger – start flows via http +## Webhook trigger – start flows via HTTP A Webhook trigger generates a unique URL that lets external applications (such as GitHub, Amazon EventBridge, or any system that can send HTTP requests) automatically start new executions in Kestra. @@ -18,7 +18,6 @@ Each webhook URL requires a secret `key` to secure it. This prevents unauthorize type: io.kestra.plugin.core.trigger.Webhook ``` -A Webhook trigger enables triggering a flow from a webhook URL. When you create the trigger, you must provide a `key`. This `key` is embedded in the webhook URL: `/api/v1/main/executions/webhook/{namespace}/{flowId}/{key}`. For security, use a randomly generated string rather than something easy to guess. Kestra accepts `GET`, `POST`, and `PUT` requests on the webhook URL. Both the request body and headers are automatically available as variables inside your flow. @@ -60,7 +59,7 @@ You can also copy the formed Webhook URL from the **Triggers** tab. ## Webhook response -By default, a webhook trigger answers with JSON. When you need the caller to wait for a custom response (e.g., validation handshakes that require `text/plain`), enable `wait` and set the `responseContentType` to `text/plain`. +By default, a webhook trigger responds with JSON. When you need the caller to wait for a custom response (e.g., validation handshakes that require `text/plain`), enable `wait` and set the `responseContentType` to `text/plain`. ```yaml triggers: @@ -72,40 +71,21 @@ triggers: responseContentType: text/plain # optional, defaults to application/json ``` -Behavior: - `wait: true` keeps the HTTP connection open until the flow finishes or hits the trigger’s timeout. - `returnOutputs: true` returns the flow outputs as the HTTP response body (JSON by default). Override with `responseContentType` for plaintext or other formats. ---- - ## Webhook trigger testing If your flow uses trigger variables (such as `{{ trigger.body }})`, you can test it directly from the execution modal. Kestra generates a ready-to-use `cURL` command that lets you trigger the flow with a custom JSON payload. ![Webhook Trigger Test](./webhook-trigger-test.png) ---- - See the [Webhook trigger plugin documentation](/plugins/core/trigger/io.kestra.plugin.core.trigger.webhook) for a full list of properties and outputs. ## Filtering webhook executions with `when` Use the `when` property to conditionally fire the trigger based on the request body or headers. The `when` value is a [Pebble expression](../../../expressions/index.mdx) evaluated against the incoming request. If the expression evaluates to a falsy value, Kestra ignores the request and no execution is created. -### Before - -```yaml -triggers: - - id: webhook - type: io.kestra.plugin.core.trigger.Webhook - key: 4wjtkzwVGBM9yKnjm3yv8r - conditions: - - type: io.kestra.plugin.core.condition.Expression - expression: "{{ trigger.body.hello == 'world' }}" -``` - -### After - ```yaml triggers: - id: webhook diff --git a/src/contents/docs/05.workflow-components/07.triggers/index.mdx b/src/contents/docs/05.workflow-components/07.triggers/index.mdx index 1e7b58a9aed..932932252d7 100644 --- a/src/contents/docs/05.workflow-components/07.triggers/index.mdx +++ b/src/contents/docs/05.workflow-components/07.triggers/index.mdx @@ -23,14 +23,10 @@ A trigger is a mechanism that automatically starts the execution of a flow. >
---- - -Triggers can be either scheduled or event-based, giving you flexibility in how you automate workflow execution. +Triggers can be either scheduled or event-based. ## Trigger types -Kestra supports both **scheduled** and **external** events. - Kestra supports five core trigger types: - [Schedule trigger](./01.schedule-trigger/index.md) allows you to execute your flow on a regular cadence e.g. using a CRON expression and custom scheduling conditions. @@ -41,7 +37,7 @@ Kestra supports five core trigger types: Many other triggers are available from the plugins, such as triggers based on file detection events, e.g. the [S3 trigger](/plugins/plugin-aws/s3/io.kestra.plugin.aws.s3.trigger), or a new message arrival in a message queue, such as the [SQS](/plugins/plugin-aws/sqs/io.kestra.plugin.aws.sqs.realtimetrigger) or [Kafka trigger](/plugins/plugin-kafka/io.kestra.plugin.kafka.trigger). -### Trigger Common Properties +### Trigger common properties The following properties are common to all triggers: @@ -55,15 +51,13 @@ The following properties are common to all triggers: | `workerGroup.key` | To execute this trigger on a specific Worker Group (EE). | | `when` | A Pebble expression that must evaluate to `true` for the trigger to fire. | ---- - ## Trigger variables Triggers expose metadata through expressions. For example: -– `{{ trigger.date }}` returns the current date for the [Schedule trigger](./01.schedule-trigger/index.md) -– `{{ trigger.uri }}` returns the file or message for file detection or message arrival events -– `{{ trigger.rows }}` provides query results for triggers like [PostgreSQL Query](/plugins/plugin-jdbc-postgres/io.kestra.plugin.jdbc.postgresql.trigger) trigger. +- `{{ trigger.date }}` returns the current date for the [Schedule trigger](./01.schedule-trigger/index.md) +- `{{ trigger.uri }}` returns the file or message for file detection or message arrival events +- `{{ trigger.rows }}` provides query results for triggers like the [PostgreSQL Query](/plugins/plugin-jdbc-postgres/io.kestra.plugin.jdbc.postgresql.trigger) trigger This example will log the date when the trigger executes the flow: @@ -84,9 +78,9 @@ triggers: ``` :::alert{type="warning"} -Keep in mind that the above-mentioned **templated variables** are only available when the execution is created **automatically** by the trigger. You'll get an error if you try to run a flow containing such variables **manually**. +These **templated variables** are only available when the execution is created **automatically** by the trigger. You'll get an error if you try to run a flow containing such variables **manually**. -Also, note that **you don't need an extra task to consume** the file or message from the event. Kestra downloads those automatically to the **internal storage** and makes those available in your flow using `{{ trigger.uri }}` variable. Therefore, you don't need any additional task to e.g. consume a message from the SQS queue or to download a file from S3 when using those event triggers. The trigger already consumes and downloads those, making them directly available for further processing. Check the documentation of a specific trigger and [Blueprints](/blueprints) with the **Trigger** tag for more details and examples. +**You don't need an extra task to consume** the file or message from the event. Kestra downloads those automatically to the **internal storage** and makes those available in your flow using `{{ trigger.uri }}` variable. Therefore, you don't need any additional task to e.g. consume a message from the SQS queue or to download a file from S3 when using those event triggers. The trigger already consumes and downloads those, making them directly available for further processing. Check the documentation of a specific trigger and [Blueprints](/blueprints) with the **Trigger** tag for more details and examples. ::: Each trigger ID is limited to a single active execution at a time. If a scheduled execution is still running, the next one will be queued instead of started immediately. For instance, if an execution from a flow with a `Schedule` trigger with ID `hourly` is still in a `Running` state, another one will not be started. However, you can still trigger the same flow manually (from the UI or API), and the scheduled executions will not be affected. @@ -248,12 +242,12 @@ When you add that flow to Kestra, you'll see that no Executions are created. To ## The `stopAfter` property -Kestra 0.15 introduced a generic `stopAfter` property which is a list of states that will disable the trigger after the flow execution has reached one of the states in the list. +The `stopAfter` property is a list of states that disable the trigger after the flow execution reaches one of those states. This property is most useful with `Schedule` triggers and polling-based triggers such as HTTP, JDBC, or File Detection. :::alert{type="info"} -Note that we don't handle any automatic trigger reenabling logic. After a trigger has been disabled due to the `stopAfter` state condition, you can take some action based on it and manually reenable the trigger. +Kestra does not automatically re-enable a trigger after it has been disabled by `stopAfter`. You must re-enable it manually once you are ready to resume. ::: ### Pause the schedule trigger after a failed execution @@ -303,7 +297,7 @@ triggers: - SUCCESS ``` -Let's break down the above example: +This example works as follows: 1. The HTTP trigger will poll the API endpoint every 30 seconds to check if the price of a product is below $110. 2. If the condition is met, the Execution will be created From f21ff0dfd0d70c50b28681cfddb49ffe9803c23a Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Mon, 20 Apr 2026 16:26:27 +0200 Subject: [PATCH 23/52] docs(loop): add new loop task Part of https://github.com/kestra-io/kestra-ee/issues/7112 --- .../01.tasks/00.flowable-tasks/index.md | 118 ++++++++++++++++++ src/contents/docs/expressions/index.mdx | 31 +++++ 2 files changed, 149 insertions(+) diff --git a/src/contents/docs/05.workflow-components/01.tasks/00.flowable-tasks/index.md b/src/contents/docs/05.workflow-components/01.tasks/00.flowable-tasks/index.md index fc96efe3cec..88e940be42c 100644 --- a/src/contents/docs/05.workflow-components/01.tasks/00.flowable-tasks/index.md +++ b/src/contents/docs/05.workflow-components/01.tasks/00.flowable-tasks/index.md @@ -279,6 +279,124 @@ Read more about performance optimization in our [best practices guides](../../.. --- +### Loop + +The `Loop` task iterates over a set of values and runs a set of child tasks for each item. Unlike `ForEach`, each iteration runs in an isolated sub-execution with its own context. + +`values` accepts a list, a JSON array string, a map, or an ION file URI. When `values` is a URI, Kestra performs one iteration per line of the file. + +```yaml +id: loop_example +namespace: company.team + +tasks: + - id: loop + type: io.kestra.plugin.core.flow.Loop + values: ["value 1", "value 2", "value 3"] + tasks: + - id: process + type: io.kestra.plugin.core.log.Log + message: "{{ item.index }} - {{ item.value }}" +``` + +Inside each iteration, use the `item` variable to access the iteration context: + +| Expression | Description | +|---|---| +| `{{ item.index }}` | Zero-based iteration index | +| `{{ item.value }}` | Current iteration value | +| `{{ item.key }}` | Current map key when `values` is a map; not set for list or URI values | +| `{{ item.parent.index }}` | Index of the nearest enclosing loop (nested loops only) | +| `{{ item.parent.value }}` | Value of the nearest enclosing loop (nested loops only) | +| `{{ item.parents[n].value }}` | Value of the nth ancestor loop, counting from innermost | + +For more details on `item`, see [loop iteration context](../../../expressions/index.mdx#loop-iteration-context) in the expressions reference. + +#### Concurrent execution + +By default (`concurrencyLimit: 1`), iterations run one at a time in order. Set `concurrencyLimit` to a higher value to run multiple iterations simultaneously, or `0` for no limit. + +```yaml +tasks: + - id: loop + type: io.kestra.plugin.core.flow.Loop + values: [1, 2, 3, 4, 5] + concurrencyLimit: 2 + tasks: + - id: process + type: io.kestra.plugin.core.log.Log + message: "Processing {{ item.value }}" +``` + +#### Failure propagation + +By default (`transmitFailed: true`), a failed iteration causes the Loop task itself to fail. Set `transmitFailed: false` to let the loop continue even when individual iterations fail. + +```yaml +tasks: + - id: loop + type: io.kestra.plugin.core.flow.Loop + values: ["value 1", "value 2", "value 3"] + transmitFailed: false + tasks: + - id: attempt + type: io.kestra.plugin.core.execution.Fail +``` + +#### Nested loops + +Loops can be nested to any depth. Because `item` is bound to the loop execution rather than individual task runs, flowable tasks nested inside a loop can access `item` directly without a `parent.` prefix. + +```yaml +tasks: + - id: outer + type: io.kestra.plugin.core.flow.Loop + values: [1, 2, 3] + tasks: + - id: inner + type: io.kestra.plugin.core.flow.Loop + values: [1, 2, 3] + tasks: + - id: log + type: io.kestra.plugin.core.log.Log + message: "outer={{ item.parent.value }} inner={{ item.value }}" +``` + +For deeper hierarchies, `item.parents[0]` is the immediate parent loop, `item.parents[1]` is the next outer loop, and so on. + +#### Loop outputs + +By default, task outputs produced inside a loop are not accessible to tasks that run after the loop. Use the `outputs` property on the Loop task to explicitly declare which values to expose. + +```yaml +tasks: + - id: loop + type: io.kestra.plugin.core.flow.Loop + values: ["a", "b", "c"] + outputs: + - id: result + type: STRING + value: "{{ outputs.process.value }}" + tasks: + - id: process + type: io.kestra.plugin.core.debug.Return + format: "processed {{ item.value }}" +``` + +The loop also exposes monitoring outputs regardless of whether `outputs` is declared: + +| Output | Description | +|---|---| +| `iterationCount` | Total number of iterations | +| `runningIterations` | Iterations still in progress | +| `terminatedIterations` | Iterations that have finished | + +The `fetchType` property controls how iteration outputs are collected: `FETCH` returns them directly in the execution context, `STORE` writes them to internal storage as a URI, and `AUTO` (the default) chooses based on whether `values` is a URI. + +For more details, see the [Loop task documentation](/plugins/core/flow/io.kestra.plugin.core.flow.loop). + +--- + ### LoopUntil `LoopUntil` runs a group of tasks repeatedly until a boolean condition evaluates to `true`. After each iteration, the task evaluates the `condition` expression; if it evaluates to `false`, the block is executed again after the configured interval. diff --git a/src/contents/docs/expressions/index.mdx b/src/contents/docs/expressions/index.mdx index 191269f5269..97cd3eb8040 100644 --- a/src/contents/docs/expressions/index.mdx +++ b/src/contents/docs/expressions/index.mdx @@ -44,6 +44,7 @@ The execution context usually includes: - `namespace` in Enterprise Edition when namespace variables are configured - `envs` for environment variables - `globals` for global configuration values +- `item` inside a [Loop](../05.workflow-components/01.tasks/00.flowable-tasks/index.md#loop) task iteration :::alert{type="info"} To inspect the full runtime context, use `{{ printContext() }}` in the Debug Expression console. @@ -113,6 +114,36 @@ When the execution is started by a `Flow` trigger: | `{{ trigger.flowId }}` | ID of the triggering flow | | `{{ trigger.flowRevision }}` | Revision of the triggering flow | +### Loop iteration context + +Inside a [Loop](../05.workflow-components/01.tasks/00.flowable-tasks/index.md#loop) task, each iteration runs as an isolated sub-execution. The `item` variable is available to all tasks within that sub-execution. + +| Expression | Description | +|---|---| +| `{{ item.index }}` | Zero-based index of the current iteration | +| `{{ item.value }}` | Value of the current iteration | +| `{{ item.key }}` | Map key of the current iteration; only set when `values` is a map | +| `{{ item.parent.index }}` | Index of the nearest enclosing loop (nested loops only) | +| `{{ item.parent.value }}` | Value of the nearest enclosing loop (nested loops only) | +| `{{ item.parents[n].value }}` | Value of the nth ancestor loop, counting from innermost (`[0]` = immediate parent) | + +Because `item` is bound to the loop execution rather than individual task runs, flowable tasks nested inside a `Loop` (such as `If` or `Parallel`) can access `item` directly without any `parent.` prefix. + +```yaml +tasks: + - id: loop + type: io.kestra.plugin.core.flow.Loop + values: ["value 1", "value 2", "value 3"] + tasks: + - id: check + type: io.kestra.plugin.core.flow.If + condition: '{{ item.value == "value 2" }}' + then: + - id: log + type: io.kestra.plugin.core.log.Log + message: "Matched at index {{ item.index }}: {{ item.value }}" +``` + ### Environment and global variables Kestra provides access to environment variables prefixed with `ENV_` by default, unless configured otherwise in the [runtime and storage configuration](/docs/configuration/runtime-and-storage). From 44ffb44b2f7592ccc21ff79e9db453a757624fcf Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Mon, 20 Apr 2026 18:10:52 +0200 Subject: [PATCH 24/52] docs(checks): improve page --- .../05.workflow-components/07.checks/index.md | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/contents/docs/05.workflow-components/07.checks/index.md b/src/contents/docs/05.workflow-components/07.checks/index.md index 2ff7b7de805..07594deb341 100644 --- a/src/contents/docs/05.workflow-components/07.checks/index.md +++ b/src/contents/docs/05.workflow-components/07.checks/index.md @@ -1,15 +1,13 @@ --- title: Checks in Kestra – Pre-Execution Validations h1: Validate Inputs Before Any Task Runs with Checks -description: Implement Checks in Kestra for pre-execution validation. Guard your workflows by enforcing conditions on inputs before any task begins execution. +description: Use checks to enforce conditions on inputs before any task runs, blocking or failing executions that don't meet your criteria. sidebarTitle: Checks icon: /src/contents/docs/icons/flow.svg version: ">= 1.2.0" --- -Add pre-execution validations that can block or fail an execution before any tasks run. - -## Add checks to validate inputs before execution +Checks are pre-execution validations that block or fail an execution before any tasks run. `checks` are flow-level assertions evaluated when validating inputs and before creating a new execution. Each check defines a boolean `when` expression and a `message` shown when the expression evaluates to false. You can choose how Kestra reacts (block, fail, or still create the execution) and how the message is styled in the UI. @@ -19,21 +17,19 @@ Checks are useful to enforce business rules on inputs (e.g., allowed values, dat Each item in `checks` supports the following properties: -- `when` *(required)*: Pebble expression that must evaluate to a boolean. For example, you can design checks against Inputs, Key-Value pairs, or other [expression](../../expressions/index.mdx) accessible workflow components. +- `when` *(required)*: Pebble expression that must evaluate to a boolean. Checks can reference inputs, key-value pairs, and other components accessible via [expressions](../../expressions/index.mdx). - `message` *(required)*: Text displayed when the condition is false. - `style` *(optional, default `INFO`)*: Visual style for the message. One of `ERROR`, `SUCCESS`, `WARNING`, `INFO`. - `behavior` *(optional, default `BLOCK_EXECUTION`)*: How the flow should react when the condition is false. One of: - `BLOCK_EXECUTION`: Do not create the execution. - `FAIL_EXECUTION`: Create the execution immediately in a failed state. - - `CREATE_EXECUTION`: Allow execution creation even if the check fails. + - `CREATE_EXECUTION`: Create the execution even when the check fails. -When clicking **Execute**, with an `ERROR` message display set in the flow code, the modal will display the `message` as soon as an input is set that doesn't satisfy the check like below: +When you click **Execute**, the modal displays the `message` as soon as an input fails a check: ![Failed Check](./checks-fail.png) ---- - -### Multiple checks +## Multiple checks If several checks fail, the most restrictive behavior wins in this priority order: `BLOCK_EXECUTION` → `FAIL_EXECUTION` → `CREATE_EXECUTION`. This lets you mix hard stops with softer warnings in the same flow. From e2c76707426246881fb44dcda95320e34df7f7df Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Tue, 21 Apr 2026 17:07:34 +0200 Subject: [PATCH 25/52] docs(2.0-migration-guide): add call out and links to metadata migrations --- .../docs/11.migration-guide/v2.0.0/index.mdx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/contents/docs/11.migration-guide/v2.0.0/index.mdx b/src/contents/docs/11.migration-guide/v2.0.0/index.mdx index 7b2cbbf2207..ee5f13ce743 100644 --- a/src/contents/docs/11.migration-guide/v2.0.0/index.mdx +++ b/src/contents/docs/11.migration-guide/v2.0.0/index.mdx @@ -2,13 +2,24 @@ title: 2.0.0 icon: /src/contents/docs/icons/migration-guide.svg release: 2.0.0 -description: Migration guides and deprecated features for Kestra version 2.0.0. +description: Migration guide for upgrading from Kestra 1.3 to 2.0.0, covering deprecated properties and trigger types that are now removed. --- import ChildCard from "~/components/docs/ChildCard.astro" -## 2.0.0 +This guide covers what you need to change when upgrading from **Kestra 1.3** to **2.0.0**. -Deprecated features and migration guides for 2.0.0 and onwards. +:::alert{type="warning"} +**Start from Kestra 1.3.** This guide assumes you are already running the latest **1.3.x** release. If you are on an older version, complete the required metadata migrations before upgrading to 2.0.0: + +- [KV Store and Secrets metadata migration](../v1.1.0/kv-secrets-metadata-migration) (introduced in 1.1) +- [Namespace Files metadata migration](../v1.2.0/namespace-file-migration) (introduced in 1.2) + +If you are upgrading directly from 1.0, the [LTS migration guide (1.0 → 1.3)](../v1.3.0/lts-migration) consolidates all required steps in one pass. +::: + +## What changed in 2.0.0 + +Kestra 2.0.0 removes several task properties and trigger condition types that were deprecated throughout the 1.x series but continued to work as aliases. If you completed the 1.3 migration, the changes in this guide are all that remain before upgrading. \ No newline at end of file From 3e1f8fd5b830ef20cf1f4bf9796fd07c848b85b3 Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Tue, 21 Apr 2026 18:06:34 +0200 Subject: [PATCH 26/52] docs(trigger-redesign): fix flows and add more to migration guide --- .../docs/03.tutorial/04.triggers/index.md | 4 +- .../docs/03.tutorial/06.errors/index.md | 4 +- .../07.triggers/02.flow-trigger/index.md | 22 +- .../05.workflow-components/18.sla/index.md | 10 +- .../03.monitoring/index.md | 18 +- .../v2.0.0/run-if-renamed-when/index.md | 13 +- .../trigger-conditions-redesign/index.md | 639 ++++++++++++++++-- .../docs/14.best-practices/0.flows/index.md | 34 +- .../docs/15.how-to-guides/alerting/index.md | 3 +- .../multiplecondition-listener/index.md | 82 +-- .../secops-with-kestra/index.md | 12 +- .../use-cases/05.approval-processes/index.md | 11 +- 12 files changed, 663 insertions(+), 189 deletions(-) diff --git a/src/contents/docs/03.tutorial/04.triggers/index.md b/src/contents/docs/03.tutorial/04.triggers/index.md index 5b69cf2e26b..79f8e634cc0 100644 --- a/src/contents/docs/03.tutorial/04.triggers/index.md +++ b/src/contents/docs/03.tutorial/04.triggers/index.md @@ -40,7 +40,9 @@ triggers: - id: flow_trigger type: io.kestra.plugin.core.trigger.Flow - when: "{{ trigger.namespace == 'company.team' and trigger.flowId == 'first_flow' }}" + dependsOn: + - flowId: first_flow + namespace: company.team ``` :::alert{type="info"} diff --git a/src/contents/docs/03.tutorial/06.errors/index.md b/src/contents/docs/03.tutorial/06.errors/index.md index ca5c1f17ddc..c392385d450 100644 --- a/src/contents/docs/03.tutorial/06.errors/index.md +++ b/src/contents/docs/03.tutorial/06.errors/index.md @@ -145,7 +145,9 @@ tasks: triggers: - id: listen type: io.kestra.plugin.core.trigger.Flow - when: "{{ trigger.executionStatus in ['FAILED', 'WARNING'] and trigger.namespace startsWith 'company.team' }}" + dependsOn: + - states: [FAILED, WARNING] + when: "{{ namespace | startsWith('company.team') }}" ``` Adding this flow ensures you receive a Slack alert for any flow failure in the `company.team` namespace. diff --git a/src/contents/docs/05.workflow-components/07.triggers/02.flow-trigger/index.md b/src/contents/docs/05.workflow-components/07.triggers/02.flow-trigger/index.md index e1e8cbfbbfd..6361fbc6e57 100644 --- a/src/contents/docs/05.workflow-components/07.triggers/02.flow-trigger/index.md +++ b/src/contents/docs/05.workflow-components/07.triggers/02.flow-trigger/index.md @@ -88,7 +88,7 @@ Like all triggers, the Flow trigger supports a top-level `when` Pebble expressio triggers: - id: after_extract type: io.kestra.plugin.core.trigger.Flow - when: "{{ not isWeekend(trigger.date) }}" + when: "{{ labels.env == 'production' }}" dependsOn: - flowId: extract namespace: company.team @@ -176,27 +176,27 @@ window: ## Scoped trigger outputs -When a Flow trigger fires, upstream execution outputs are available under `trigger.outputs`. Outputs are scoped by namespace and flow ID to avoid key collisions when multiple upstream flows are involved: +When a Flow trigger fires, upstream execution outputs are available under `trigger.outputs`. Outputs are scoped by flow ID to avoid key collisions when multiple upstream flows are involved: ``` -trigger.outputs... +trigger.outputs.. ``` -For example, to pass an output from an upstream flow named `extract` in the `company.team` namespace: +For example, to pass an output from an upstream flow named `extract`: ```yaml triggers: - id: after_extract type: io.kestra.plugin.core.trigger.Flow inputs: - date: "{{ trigger.outputs.company.team.extract.date }}" + date: "{{ trigger.outputs.extract.date }}" dependsOn: - flowId: extract namespace: company.team ``` :::alert{type="warning"} -The output scoping format changed in Kestra 2.0. If you previously used `trigger.outputs.` (a flat map), update your expressions to the new `trigger.outputs...` format. +The output scoping format changed in Kestra 2.0. If you previously used `trigger.outputs.` (a flat map), update your expressions to the new `trigger.outputs..` format. ::: ## Label-based filtering @@ -312,7 +312,7 @@ triggers: ## Example: passing upstream outputs downstream -Reference upstream outputs using the scoped path `trigger.outputs...`: +Reference upstream outputs using the scoped path `trigger.outputs..`: ```yaml id: flow_b @@ -331,10 +331,10 @@ triggers: - id: upstream_dep type: io.kestra.plugin.core.trigger.Flow inputs: - value_from_a: "{{ trigger.outputs.kestra.sandbox.flow_a.return_value }}" + value_from_a: "{{ trigger.outputs.flow_a.return_value }}" dependsOn: - flowId: flow_a - namespace: kestra.sandbox + namespace: company.team states: [SUCCESS] ``` @@ -346,8 +346,8 @@ triggers: If an `inputs` expression on a Flow trigger fails to render — for example, because an upstream output key does not exist — Kestra creates a `FAILED` execution instead of silently dropping the event. This makes failures visible in the UI and actionable via alerting. -## Deprecated: `preconditions` and `conditions` +## Removed: `preconditions` and `conditions` -The `preconditions` and `conditions` properties are still functional in Kestra 2.0 but are deprecated and will be removed in a future release. Migrate to `dependsOn` at your earliest convenience. +The `preconditions` and `conditions` properties are removed in Kestra 2.0. Flows that still use them will fail to parse after upgrading. Migrate to `dependsOn`. See the [trigger conditions migration guide](../../../11.migration-guide/v2.0.0/trigger-conditions-redesign/index.md) for a complete before/after reference. diff --git a/src/contents/docs/05.workflow-components/18.sla/index.md b/src/contents/docs/05.workflow-components/18.sla/index.md index b5de120c48a..34d3915b4e2 100644 --- a/src/contents/docs/05.workflow-components/18.sla/index.md +++ b/src/contents/docs/05.workflow-components/18.sla/index.md @@ -111,12 +111,10 @@ tasks: triggers: - id: alert_on_failure type: io.kestra.plugin.core.trigger.Flow - labels: - sla: miss - states: - - FAILED - - WARNING - - CANCELLED + dependsOn: + - labels: + sla: miss + states: [FAILED, WARNING, CANCELLED] ``` :::alert{type="info"} diff --git a/src/contents/docs/10.administrator-guide/03.monitoring/index.md b/src/contents/docs/10.administrator-guide/03.monitoring/index.md index 52138681d74..01c99a8d633 100644 --- a/src/contents/docs/10.administrator-guide/03.monitoring/index.md +++ b/src/contents/docs/10.administrator-guide/03.monitoring/index.md @@ -52,15 +52,17 @@ tasks: triggers: - id: listen type: io.kestra.plugin.core.trigger.Flow - when: "{{ trigger.executionStatus in ['FAILED', 'WARNING'] and trigger.namespace startsWith 'company.analytics' }}" + dependsOn: + - states: [FAILED, WARNING] + when: "{{ namespace | startsWith('company.analytics') }}" ``` Adding this single flow will ensure that you receive a Slack alert on any flow failure in the `company.analytics` namespace. Here is an example alert notification: ![alert notification](../../03.tutorial/06.errors/alert-notification.png) -:::alert{type="warning"} -Note that if you want this alert to be sent on failure across multiple namespaces, combine conditions using `or` in the `when` Pebble expression. See the example below: +:::alert{type="info"} +To alert on failures across multiple namespaces or specific flows, use `mode: ANY` with multiple `dependsOn` entries. The trigger fires when any entry is satisfied: ```yaml id: alert namespace: company.system @@ -75,11 +77,17 @@ tasks: triggers: - id: listen type: io.kestra.plugin.core.trigger.Flow - when: "{{ trigger.executionStatus in ['FAILED', 'WARNING'] and (trigger.namespace startsWith 'company.product' or (trigger.flowId == 'cleanup' and trigger.namespace == 'company.system')) }}" + mode: ANY + dependsOn: + - states: [FAILED, WARNING] + when: "{{ namespace | startsWith('company.product') }}" + - flowId: cleanup + namespace: company.system + states: [FAILED, WARNING] ``` ::: -The example above works correctly because `or` is used explicitly in the Pebble expression. If you combine two conditions with `and` where only one can ever be true at a time, no alerts will be sent. For example, a flow execution can only belong to one namespace at a time, so requiring it to match two different namespace prefixes simultaneously will never fire. Make sure to use `or` for alternatives and `and` only for conditions that can both be true at the same time. +The example above fires when either any `company.product` flow fails or the specific `cleanup` flow in `company.system` fails. `mode: ANY` means the trigger fires as soon as one entry is satisfied — you do not need to combine everything into a single expression. ## Monitoring diff --git a/src/contents/docs/11.migration-guide/v2.0.0/run-if-renamed-when/index.md b/src/contents/docs/11.migration-guide/v2.0.0/run-if-renamed-when/index.md index d47244d4923..8aea5ffccf5 100644 --- a/src/contents/docs/11.migration-guide/v2.0.0/run-if-renamed-when/index.md +++ b/src/contents/docs/11.migration-guide/v2.0.0/run-if-renamed-when/index.md @@ -40,9 +40,9 @@ tasks: The behavior is identical — the same Pebble rendering and `SKIPPED` state logic apply. -## Triggers: conditions → when +## Triggers: conditions → dependsOn (Flow triggers) -The `conditions` list on triggers is deprecated. Replace it with a single `when` Pebble expression. +The `conditions` list on Flow triggers is replaced by `dependsOn`. State filtering moves to the `states` property on each entry. ### Before @@ -62,13 +62,18 @@ triggers: triggers: - id: on_success type: io.kestra.plugin.core.trigger.Flow - when: "{{ trigger.executionStatus == 'SUCCESS' }}" + dependsOn: + - flowId: upstream_flow + namespace: company.team + states: [SUCCESS] ``` +For the full before/after reference for Flow trigger conditions, including namespace filtering, label matching, and `preconditions` migration, see the [trigger conditions redesign guide](../trigger-conditions-redesign/index.md). + ## Migration steps 1. **Search your flows** for `runIf:` and replace each occurrence with `when:`. The property value and any Pebble expressions stay the same. -2. **Search your flows** for `conditions:` on trigger blocks and replace them with an equivalent `when:` Pebble expression. +2. **Search your flows** for `conditions:` on Flow trigger blocks and replace them with `dependsOn` entries. See the [trigger conditions redesign guide](../trigger-conditions-redesign/index.md) for before/after examples. 3. **Validate** by saving the updated flows in the Kestra UI or via the API. :::alert{type="warning"} diff --git a/src/contents/docs/11.migration-guide/v2.0.0/trigger-conditions-redesign/index.md b/src/contents/docs/11.migration-guide/v2.0.0/trigger-conditions-redesign/index.md index f6ba918ff30..55a9c47b898 100644 --- a/src/contents/docs/11.migration-guide/v2.0.0/trigger-conditions-redesign/index.md +++ b/src/contents/docs/11.migration-guide/v2.0.0/trigger-conditions-redesign/index.md @@ -4,21 +4,54 @@ sidebarTitle: Trigger Conditions Redesign icon: /src/contents/docs/icons/migration-guide.svg release: 2.0.0 editions: ["OSS", "EE"] -description: The conditions list on triggers is deprecated in Kestra 2.0 in favor of a when Pebble expression. The preconditions property on Flow triggers is replaced by dependsOn and window. +description: The conditions list on triggers and the preconditions block on Flow triggers are removed in Kestra 2.0. Schedule and Webhook triggers use a top-level when expression. Flow triggers use dependsOn and window. --- -Kestra 2.0 simplifies and unifies how trigger conditions are expressed. +Kestra 2.0 replaces the `conditions` and `preconditions` system across all trigger types. -- The `conditions` list on all triggers is **deprecated** in favor of a single `when` Pebble expression string. -- The `preconditions` property on Flow triggers is **deprecated** in favor of `dependsOn` (for upstream flow entries) and `window` (for time windows). -- Flow trigger outputs are now **scoped** by namespace and flow ID: `trigger.outputs...`. -- Input rendering failures on Flow triggers now create a **`FAILED` execution** instead of silently dropping the event. +- **Schedule and Webhook triggers** — the `conditions` list is removed in favor of a top-level `when` Pebble expression. +- **Flow triggers** — both `conditions` and `preconditions` are removed in favor of `dependsOn` (upstream flow entries) and `window` (time window configuration). +- **Flow trigger outputs** — scoped by flow ID: `trigger.outputs..`. +- **Input rendering failures** — now create a `FAILED` execution instead of silently dropping the event. -All deprecated properties remain functional in 2.0.0. Migrate at your earliest convenience to avoid a hard break when they are removed in a future release. +Both `conditions` and `preconditions` are removed in Kestra 2.0. Flows that still use them will fail to parse after upgrading. -## `conditions` → `when` on all trigger types +## `conditions` → `when` on Schedule and Webhook triggers -### Schedule trigger: specific day of week +### What replaces what + +| Old condition type | New `when` expression | +|---|---| +| `DayWeek` (e.g. MONDAY) | `{{ dayOfWeek(trigger.date) == 'MONDAY' }}` | +| `Weekend` | `{{ isWeekend(trigger.date) }}` | +| `Not` > `Weekend` (weekdays only) | `{{ not isWeekend(trigger.date) }}` | +| `Not` > `DayWeek` SUNDAY (exclude Sundays) | `{{ dayOfWeek(trigger.date) != 'SUNDAY' }}` | +| `PublicHoliday` (country: FR) | `{{ isPublicHoliday(trigger.date, 'FR') }}` | +| `Not` > `PublicHoliday` + `Weekend` (workdays) | `{{ not isWeekend(trigger.date) and not isPublicHoliday(trigger.date, 'FR') }}` | +| `DayWeekInMonth` (MONDAY, FIRST) | `{{ isDayWeekInMonth(trigger.date, 'MONDAY', 'FIRST') }}` | +| `DateTimeBetween` (after/before) | `{{ trigger.date > '2025-12-31T23:59:59Z' and trigger.date < '2026-06-30T23:59:59Z' }}` | +| `TimeBetween` (08:00-17:00) | `{{ hourOfDay(trigger.date) >= 8 and hourOfDay(trigger.date) < 17 }}` | +| `Expression` (custom Pebble) | Direct `when` expression, no wrapper needed | +| `Expression` on webhook body/headers | `{{ trigger.body.field == 'value' }}` or `{{ trigger.headers['X-Key'] == 'value' }}` | +| Multiple `Expression` conditions | Combined with `and` / `or` in a single `when` | + +### New Pebble helper functions + +These functions are introduced specifically for `when` expressions to replace verbose date formatting patterns: + +| Function | Signature | Description | +|---|---|---| +| `isPublicHoliday` | `isPublicHoliday(date, countryCode[, subDivision])` | Returns `true` if the date is a public holiday. Backed by Jollyday. Optional third argument for sub-divisions (e.g. `'IDF'`). | +| `isDayWeekInMonth` | `isDayWeekInMonth(date, dayOfWeek, position)` | Returns `true` if the date is the Nth occurrence of a weekday in its month. `position` accepts `FIRST`, `SECOND`, `THIRD`, `FOURTH`, or `LAST`. | +| `isWeekend` | `isWeekend(date)` | Returns `true` if the date falls on Saturday or Sunday. | +| `dayOfWeek` | `dayOfWeek(date)` | Returns the day name as a string (`MONDAY`, `TUESDAY`, …, `SUNDAY`). | +| `hourOfDay` | `hourOfDay(date)` | Returns the hour as an integer (0–23). | +| `dayOfMonth` | `dayOfMonth(date)` | Returns the day of the month as an integer (1–31). | +| `monthOfYear` | `monthOfYear(date)` | Returns the month as an integer (1–12). | + +Existing Pebble filters (`startsWith`, `endsWith`, `date`) and operators (`and`, `or`, `not`, `==`, `!=`, `>`, `<`, `>=`, `<=`) cover the remaining use cases. + +### Schedule: specific day of week **Before** @@ -42,7 +75,7 @@ triggers: when: "{{ dayOfWeek(trigger.date) == 'MONDAY' }}" ``` -### Schedule trigger: weekends only +### Schedule: weekends only **Before** @@ -65,7 +98,58 @@ triggers: when: "{{ isWeekend(trigger.date) }}" ``` -### Schedule trigger: public holidays +### Schedule: weekdays only (exclude weekends) + +**Before** + +```yaml +triggers: + - id: schedule + type: io.kestra.plugin.core.trigger.Schedule + cron: "0 9 * * *" + conditions: + - type: io.kestra.plugin.core.condition.Not + conditions: + - type: io.kestra.plugin.core.condition.Weekend +``` + +**After** + +```yaml +triggers: + - id: schedule + type: io.kestra.plugin.core.trigger.Schedule + cron: "0 9 * * *" + when: "{{ not isWeekend(trigger.date) }}" +``` + +### Schedule: exclude Sundays + +**Before** + +```yaml +triggers: + - id: schedule + type: io.kestra.plugin.core.trigger.Schedule + cron: "0 9 * * *" + conditions: + - type: io.kestra.plugin.core.condition.Not + conditions: + - type: io.kestra.plugin.core.condition.DayWeek + dayOfWeek: SUNDAY +``` + +**After** + +```yaml +triggers: + - id: schedule + type: io.kestra.plugin.core.trigger.Schedule + cron: "0 9 * * *" + when: "{{ dayOfWeek(trigger.date) != 'SUNDAY' }}" +``` + +### Schedule: public holidays **Before** @@ -89,7 +173,9 @@ triggers: when: "{{ isPublicHoliday(trigger.date, 'FR') }}" ``` -### Schedule trigger: workdays only (not weekend, not public holiday) +With a sub-division: `{{ isPublicHoliday(trigger.date, 'FR', 'IDF') }}`. + +### Schedule: workdays only (not weekend, not public holiday) **Before** @@ -116,7 +202,7 @@ triggers: when: "{{ not isWeekend(trigger.date) and not isPublicHoliday(trigger.date, 'FR') }}" ``` -### Schedule trigger: first Monday of the month +### Schedule: first Monday of the month **Before** @@ -141,7 +227,7 @@ triggers: when: "{{ isDayWeekInMonth(trigger.date, 'MONDAY', 'FIRST') }}" ``` -### Schedule trigger: date range +### Schedule: date range **Before** @@ -166,7 +252,61 @@ triggers: when: "{{ trigger.date > '2025-12-31T23:59:59Z' and trigger.date < '2026-06-30T23:59:59Z' }}" ``` -### Webhook trigger: filter by body or headers +### Schedule: specific hours only + +**Before** + +```yaml +triggers: + - id: schedule + type: io.kestra.plugin.core.trigger.Schedule + cron: "0 * * * *" + conditions: + - type: io.kestra.plugin.core.condition.TimeBetween + after: "08:00:00" + before: "17:00:00" +``` + +**After** + +```yaml +triggers: + - id: schedule + type: io.kestra.plugin.core.trigger.Schedule + cron: "0 * * * *" + when: "{{ hourOfDay(trigger.date) >= 8 and hourOfDay(trigger.date) < 17 }}" +``` + +### Schedule: combining multiple conditions + +**Before** (first Monday of the month, skip public holidays in France) + +```yaml +triggers: + - id: schedule + type: io.kestra.plugin.core.trigger.Schedule + cron: "0 11 * * *" + conditions: + - type: io.kestra.plugin.core.condition.DayWeekInMonth + dayOfWeek: MONDAY + dayInMonth: FIRST + - type: io.kestra.plugin.core.condition.Not + conditions: + - type: io.kestra.plugin.core.condition.PublicHoliday + country: FR +``` + +**After** + +```yaml +triggers: + - id: schedule + type: io.kestra.plugin.core.trigger.Schedule + cron: "0 11 * * *" + when: "{{ isDayWeekInMonth(trigger.date, 'MONDAY', 'FIRST') and not isPublicHoliday(trigger.date, 'FR') }}" +``` + +### Webhook: filter by body **Before** @@ -190,16 +330,77 @@ triggers: when: "{{ trigger.body.hello == 'world' }}" ``` -For the full list of Pebble calendar helper functions (`isWeekend`, `isPublicHoliday`, `isDayWeekInMonth`, etc.), see the [date and calendar helpers](../../../expressions/index.mdx#date-and-calendar-helpers) reference. +### Webhook: filter by header and body -## `preconditions` → `dependsOn` on Flow triggers +**Before** -The `preconditions` block is replaced by two top-level properties on the Flow trigger: +```yaml +triggers: + - id: webhook + type: io.kestra.plugin.core.trigger.Webhook + key: myKey + conditions: + - type: io.kestra.plugin.core.condition.Expression + expression: "{{ trigger.headers['X-Event-Type'] == 'deploy' }}" + - type: io.kestra.plugin.core.condition.Expression + expression: "{{ trigger.body.environment == 'production' }}" +``` + +**After** + +```yaml +triggers: + - id: webhook + type: io.kestra.plugin.core.trigger.Webhook + key: myKey + when: "{{ trigger.headers['X-Event-Type'] == 'deploy' and trigger.body.environment == 'production' }}" +``` + +Multiple `Expression` conditions combine into a single `when` expression using `and` / `or`. + +For the full list of Pebble calendar helper functions (`isWeekend`, `isPublicHoliday`, `isDayWeekInMonth`, `hourOfDay`, etc.), see the [date and calendar helpers](../../../expressions/index.mdx#date-and-calendar-helpers) reference. + +## `conditions` and `preconditions` → `dependsOn` on Flow triggers + +Both `conditions` (execution-level condition types such as `ExecutionStatus`, `ExecutionFlow`, `ExecutionNamespace`) and `preconditions` (upstream flow lists with time windows) are replaced by a single `dependsOn` list. Each entry declares one upstream dependency with typed properties. + +### What replaces what + +| Old property / condition type | New equivalent | +|---|---| +| `conditions` list on Flow trigger | `dependsOn` list | +| `preconditions` block | `dependsOn` list + `window` | +| `multipleConditions` block | `dependsOn` list + `window.every` | +| `ExecutionStatus` (`in: [SUCCESS]`) | `states: [SUCCESS]` on the `dependsOn` entry | +| `ExecutionFlow` (`flowId`, `namespace`) | `flowId` + `namespace` on the `dependsOn` entry | +| `ExecutionNamespace` (exact) | `namespace` on the `dependsOn` entry | +| `ExecutionNamespace` (`comparison: PREFIX`) | `when: "{{ namespace \| startsWith('...') }}"` on the entry | +| `ExecutionLabels` (`labels: {k: v}`) | `labels: {k: v}` on the `dependsOn` entry | +| `ExecutionOutputs` (`expression`) | `when` with `outputs.` on the entry | +| `HasRetryAttempt` | `when: "{{ hasRetryAttempt == true }}"` on the entry | +| `Not` > `ExecutionStatus` | Explicit `states` list or `when: "{{ state != 'SUCCESS' }}"` | +| Multiple triggers for OR logic | `mode: ANY` with `dependsOn` entries | +| `preconditions.resetOnSuccess: true` | `window.fireOnce: true` | +| `timeWindow.type: DAILY_TIME_DEADLINE` | `window.deadline` | +| `timeWindow.type: DAILY_TIME_WINDOW` | `window.from` + `window.to` | +| `timeWindow.type: DURATION_WINDOW` | `window.every` | +| `timeWindow.type: SLIDING_WINDOW` | `window.lookback` | + +### `dependsOn` entry properties + +| `dependsOn` entry property | Type | Default | Description | +|---|---|---|---| +| `flowId` | string | — | Exact flow ID to match. Omit to match any flow. | +| `namespace` | string | — | Exact namespace to match. Use `when` for prefix or pattern matching. | +| `states` | list | `[SUCCESS, WARNING]` | Execution states that satisfy this entry. | +| `labels` | map | — | Labels the upstream execution must carry (all must match). | +| `when` | string | — | Pebble expression for additional filtering on the upstream execution context. | -- `dependsOn` — a list of upstream flow entries that must all be satisfied. -- `window` — controls the time window over which upstream executions are accumulated. +:::alert{type="warning"} +The default `states` changed from `[SUCCESS, WARNING, PAUSED]` to `[SUCCESS, WARNING]`. If your flows relied on `PAUSED` being included by default, add it explicitly: `states: [SUCCESS, WARNING, PAUSED]`. +::: -### Single upstream flow +### Single upstream flow (from `preconditions`) **Before** @@ -227,6 +428,33 @@ triggers: states: [SUCCESS] ``` +### Single upstream flow (from `conditions`) + +**Before** + +```yaml +triggers: + - id: on_completion + type: io.kestra.plugin.core.trigger.Flow + states: [SUCCESS] + conditions: + - type: io.kestra.plugin.core.condition.ExecutionFlow + namespace: company.team + flowId: extract +``` + +**After** + +```yaml +triggers: + - id: on_completion + type: io.kestra.plugin.core.trigger.Flow + dependsOn: + - flowId: extract + namespace: company.team + states: [SUCCESS] +``` + ### Multiple upstream flows with a deadline **Before** @@ -264,7 +492,49 @@ triggers: deadline: "09:00:00+01:00" ``` -The default `states` for a `dependsOn` entry is `[SUCCESS, WARNING]`. Specify `states` explicitly when you need a different set. +`states` defaults to `[SUCCESS, WARNING]`. `window` moves to the trigger level. See [Window configuration](#window-configuration) for all window types and the `onMiss` property. + +### Multiple upstream flows (from `multipleConditions`) + +**Before** + +```yaml +triggers: + - id: multiple_listen_flow + type: io.kestra.plugin.core.trigger.Flow + multipleConditions: + - id: multiple + window: P1D + windowAdvance: P0D + conditions: + flow_a: + type: io.kestra.plugin.core.condition.ExecutionFlow + namespace: company.team + flowId: multiplecondition_flow_a + flow_b: + type: io.kestra.plugin.core.condition.ExecutionFlow + namespace: company.team + flowId: multiplecondition_flow_b +``` + +**After** + +```yaml +triggers: + - id: multiple_listen_flow + type: io.kestra.plugin.core.trigger.Flow + dependsOn: + - flowId: multiplecondition_flow_a + namespace: company.team + states: [SUCCESS] + - flowId: multiplecondition_flow_b + namespace: company.team + states: [SUCCESS] + window: + every: P1D +``` + +The arbitrary string keys (`flow_a`, `flow_b`) are dropped. `dependsOn` is always a list. ### Namespace-wide alerting (prefix matching) @@ -295,6 +565,8 @@ triggers: when: "{{ namespace | startsWith('company') }}" ``` +`namespace` in `dependsOn` is an exact match. Use `when` with `startsWith` for prefix matching. + ### Label-based filtering **Before** @@ -329,46 +601,60 @@ triggers: **Before** ```yaml -preconditions: - id: my_filter - where: - - id: flow1 - filters: - - field: NAMESPACE - type: STARTS_WITH - value: io.kestra.tests - - field: EXPRESSION - type: IS_TRUE - value: "{{ labels.some == 'label' }}" +triggers: + - id: after_extract + type: io.kestra.plugin.core.trigger.Flow + preconditions: + id: my_filter + where: + - id: flow1 + filters: + - field: NAMESPACE + type: STARTS_WITH + value: io.kestra.tests + - field: EXPRESSION + type: IS_TRUE + value: "{{ labels.some == 'label' }}" ``` **After** ```yaml -dependsOn: - - when: "{{ namespace | startsWith('io.kestra.tests') }}" - states: [SUCCESS] - labels: - some: label +triggers: + - id: after_extract + type: io.kestra.plugin.core.trigger.Flow + dependsOn: + - when: "{{ namespace | startsWith('io.kestra.tests') }}" + states: [SUCCESS] + labels: + some: label ``` +`labels` handles exact key-value matching declaratively. `when` handles everything else. + ### Filtering on upstream execution outputs **Before** ```yaml -conditions: - - type: io.kestra.plugin.core.condition.ExecutionOutputs - expression: "{{ outputs.row_count > 0 }}" +triggers: + - id: after_extract + type: io.kestra.plugin.core.trigger.Flow + conditions: + - type: io.kestra.plugin.core.condition.ExecutionOutputs + expression: "{{ outputs.row_count > 0 }}" ``` **After** ```yaml -dependsOn: - - flowId: extract - namespace: company.team - when: "{{ outputs.row_count > 0 }}" +triggers: + - id: after_extract + type: io.kestra.plugin.core.trigger.Flow + dependsOn: + - flowId: extract + namespace: company.team + when: "{{ outputs.row_count > 0 }}" ``` ### Filtering on retry attempts @@ -396,6 +682,45 @@ triggers: when: "{{ hasRetryAttempt == true }}" ``` +### Negation: trigger on any state except SUCCESS + +**Before** + +```yaml +triggers: + - id: on_non_success + type: io.kestra.plugin.core.trigger.Flow + conditions: + - type: io.kestra.plugin.core.condition.Not + conditions: + - type: io.kestra.plugin.core.condition.ExecutionStatus + in: [SUCCESS] +``` + +**After (option 1 — explicit states)** + +```yaml +triggers: + - id: on_non_success + type: io.kestra.plugin.core.trigger.Flow + dependsOn: + - flowId: extract + namespace: company.team + states: [FAILED, WARNING, KILLED, CANCELLED] +``` + +**After (option 2 — `when` expression)** + +```yaml +triggers: + - id: on_non_success + type: io.kestra.plugin.core.trigger.Flow + dependsOn: + - flowId: extract + namespace: company.team + when: "{{ state != 'SUCCESS' }}" +``` + ### Mixed triggers: success and failure on the same upstream flow **Before** @@ -438,11 +763,107 @@ triggers: states: [FAILED] ``` +Same `dependsOn` syntax regardless of whether the original used `conditions` or `preconditions`. + +### OR logic: fire when any upstream completes + +Previously, OR logic required N separate Flow triggers. `mode: ANY` consolidates them into one. + +**Before** (two separate triggers) + +```yaml +triggers: + - id: on_salesforce + type: io.kestra.plugin.core.trigger.Flow + conditions: + - type: io.kestra.plugin.core.condition.ExecutionFlow + namespace: company.sources + flowId: ingest_salesforce + - type: io.kestra.plugin.core.condition.ExecutionStatus + in: [SUCCESS] + - id: on_hubspot + type: io.kestra.plugin.core.trigger.Flow + conditions: + - type: io.kestra.plugin.core.condition.ExecutionFlow + namespace: company.sources + flowId: ingest_hubspot + - type: io.kestra.plugin.core.condition.ExecutionStatus + in: [SUCCESS] +``` + +**After** + +```yaml +triggers: + - id: react_to_any_source + type: io.kestra.plugin.core.trigger.Flow + mode: ANY + dependsOn: + - flowId: ingest_salesforce + namespace: company.sources + states: [SUCCESS] + - flowId: ingest_hubspot + namespace: company.sources + states: [SUCCESS] +``` + +`mode: ANY` fires as soon as either dependency is satisfied. The default `mode: ALL` requires every entry to be satisfied before the trigger fires. + +### `mode` values + +| Value | Behavior | Required properties | +|---|---|---| +| `ALL` (default) | Fires when all `dependsOn` entries are satisfied | — | +| `ANY` | Fires as soon as any one entry is satisfied | — | +| `AT_LEAST` | Fires when at least `minSatisfied` entries are satisfied | `minSatisfied` (integer ≥ 1, ≤ entry count) | + +### OR logic with a time window + +```yaml +triggers: + - id: daily_any_source + type: io.kestra.plugin.core.trigger.Flow + mode: ANY + dependsOn: + - flowId: ingest_salesforce + namespace: company.sources + - flowId: ingest_hubspot + namespace: company.sources + window: + deadline: "09:00:00" +``` + +Fire before 9 AM when either source completes. + +### N of M: at least 2 out of 3 + +```yaml +triggers: + - id: partial_success + type: io.kestra.plugin.core.trigger.Flow + mode: AT_LEAST + minSatisfied: 2 + dependsOn: + - flowId: ingest_salesforce + namespace: company.sources + states: [SUCCESS] + - flowId: ingest_hubspot + namespace: company.sources + states: [SUCCESS] + - flowId: ingest_zendesk + namespace: company.sources + states: [SUCCESS] + window: + deadline: "09:00:00" +``` + +`mode: AT_LEAST` fires when `minSatisfied` entries are satisfied. `minSatisfied` must be >= 1 and <= the number of `dependsOn` entries. + ## Scoped trigger outputs -In Kestra 2.0, Flow trigger outputs are scoped by namespace and flow ID to prevent key collisions when multiple upstream flows are involved. +Flow trigger outputs are scoped by flow ID to prevent key collisions when multiple upstream flows are involved. -**Before** (flat map, all upstream outputs merged together) +**Before** (flat map — all upstream outputs merged together) ```yaml triggers: @@ -458,25 +879,124 @@ triggers: states: [SUCCESS] ``` -**After** (scoped by namespace, then flow ID) +**After** (scoped by flow ID) ```yaml triggers: - id: after_extract type: io.kestra.plugin.core.trigger.Flow inputs: - date: "{{ trigger.outputs.company.team.extract.date }}" + date: "{{ trigger.outputs.extract.date }}" dependsOn: - flowId: extract namespace: company.team ``` -The new path format is: `trigger.outputs...` +The path format is `trigger.outputs..`. For multi-flow triggers, each upstream flow's outputs are accessed separately: + +```yaml +dependsOn: + - flowId: stg_sales + namespace: company.team + - flowId: stg_marketing + namespace: company.team +``` + +Access as `{{ trigger.outputs.stg_sales.row_count }}` and `{{ trigger.outputs.stg_marketing.row_count }}`. :::alert{type="warning"} -This is a breaking change for any flow that reads from `trigger.outputs` without a namespace and flow ID prefix. Update all such expressions when migrating to Kestra 2.0. +This is a breaking change. Update all `trigger.outputs.` references to `trigger.outputs..` when migrating to Kestra 2.0. ::: +## Window configuration + +The `window` property controls how Kestra accumulates upstream executions before evaluating `dependsOn` entries. Set exactly one group of properties per window; combining groups is a validation error. + +### Deadline + +All upstream flows must complete before a fixed time each day: + +```yaml +window: + deadline: "09:00:00+01:00" +``` + +### Daily time range + +Only executions that completed within a specific time range each day count: + +```yaml +window: + from: "06:00:00" + to: "12:00:00" +``` + +### Fixed interval + +Recurring window of a fixed size, with an optional offset from midnight: + +```yaml +window: + every: PT1D + offset: PT6H +``` + +### Lookback + +Count executions that completed within the past duration, relative to the current evaluation time: + +```yaml +window: + lookback: PT1H +``` + +### Fire once per window + +Set `fireOnce: true` to ensure the trigger fires at most once per window period, even if conditions are satisfied multiple times within it: + +```yaml +window: + deadline: "09:00:00+01:00" + fireOnce: true +``` + +Default is `false` — the trigger fires every time conditions are met within the window. + +### SLA misses with `onMiss` + +Declare what happens when the deadline passes without all dependencies being satisfied: + +```yaml +triggers: + - id: after_staging + type: io.kestra.plugin.core.trigger.Flow + dependsOn: + - flowId: stg_sales + namespace: company.team + - flowId: stg_marketing + namespace: company.team + window: + deadline: "09:00:00+01:00" + onMiss: + behavior: FAIL + labels: + sla: miss + reason: upstreamNotFinishedOnTime +``` + +`behavior: FAIL` creates a `FAILED` execution when the deadline passes. Labels are applied to that execution for downstream alerting. + +### Replacing `timeWindow` types + +| Old `timeWindow.type` | New `window` property | +|---|---| +| `DAILY_TIME_DEADLINE` | `deadline: "09:00:00+01:00"` | +| `DAILY_TIME_WINDOW` | `from: "06:00:00"` + `to: "12:00:00"` | +| `DURATION_WINDOW` | `every: PT1D` + optional `offset: PT6H` | +| `SLIDING_WINDOW` | `lookback: PT1H` | + +`preconditions.resetOnSuccess: true` maps to `window.fireOnce: true`. + ## Silent failures → FAILED executions Previously, if the `inputs` expression on a Flow trigger failed to render (for example, because an upstream output key did not exist), the trigger silently dropped the event and no execution was created. In Kestra 2.0, a `FAILED` execution is created instead. This makes failures visible in the Kestra UI and allows you to configure downstream alerting. @@ -485,15 +1005,14 @@ No migration action is required. Review your Flow trigger `inputs` expressions a ## Stable condition IDs -`dependsOn` condition IDs are now derived from a stable hash of each entry's `namespace`, `flowId`, `when`, `states`, and `labels`. Previously, condition IDs were auto-incremented (`condition_1`, `condition_2`, …) and reordering entries would reset the accumulated window state. No action is required. +`dependsOn` condition IDs are derived from a stable hash of each entry's `namespace`, `flowId`, `when`, `states`, and `labels`. Previously, condition IDs were auto-incremented (`condition_1`, `condition_2`, …) and reordering entries would reset the accumulated window state. -## Migration steps +Existing state store entries from `preconditions` use `preconditions.id` as their key. After upgrading, in-flight multi-flow triggers re-evaluate from scratch. For most deployments this means at most one missed trigger cycle. -1. **Search your flows for `conditions:` on trigger blocks** and replace each with an equivalent `when:` Pebble expression. Use the before/after examples above as a reference. -2. **Search your flows for `preconditions:` on Flow triggers** and replace with `dependsOn:` and (if applicable) `window:`. -3. **Update `trigger.outputs` references** from `trigger.outputs.` to `trigger.outputs...`. -4. **Validate** by saving the updated flows in the Kestra UI or via the API and confirming that they parse without warnings. +## Migration steps -:::alert{type="warning"} -Both `conditions` and `preconditions` will be removed in a future release. Flows that still use them will fail to parse after the aliases are dropped. -::: +1. **Replace `conditions:` on Schedule and Webhook triggers** with a `when:` Pebble expression using the before/after examples above as a reference. +2. **Replace `conditions:` and `preconditions:` on Flow triggers** with `dependsOn:` entries and (if applicable) `window:`. +3. **Update `trigger.outputs` references** from `trigger.outputs.` to `trigger.outputs..`. +4. **Update `timeWindow` to `window`** using the property mapping table above. +5. **Validate** by saving the updated flows in the Kestra UI or via the API and confirming that they parse without errors. diff --git a/src/contents/docs/14.best-practices/0.flows/index.md b/src/contents/docs/14.best-practices/0.flows/index.md index 8a5d046eab5..19051957554 100644 --- a/src/contents/docs/14.best-practices/0.flows/index.md +++ b/src/contents/docs/14.best-practices/0.flows/index.md @@ -74,38 +74,16 @@ This helps prevent stalled executions and ensures resource efficiency. ## Flow trigger on state change -Kestra can automatically start a flow as soon as another flow completes. This makes it easy to create dependencies between flows, even when they are owned by different teams. For example, a flow can trigger based on the `state` of another flow’s execution. There are multiple ways to configure this behavior, but one approach is recommended as a best practice. - -Take the following two triggers polling one specific flow: one using `preconditions.flows.states` to define the required `states` and the other using the `states` property. - -**Option 1** - -```yaml -triggers: - - id: release - type: io.kestra.plugin.core.trigger.Flow - preconditions: - id: flows - flows: - - namespace: company.release - flowId: parent - states: - - SUCCESS -``` - -or **Option 2** +Kestra can automatically start a flow as soon as another flow completes. This makes it easy to create dependencies between flows, even when they are owned by different teams. Use `dependsOn` to declare the upstream flow and the required states: ```yaml triggers: - id: release type: io.kestra.plugin.core.trigger.Flow - states: - - SUCCESS - preconditions: - id: flows - flows: - - namespace: company.release - flowId: parent + dependsOn: + - namespace: company.release + flowId: parent + states: [SUCCESS] ``` -While both configurations will work, **Option 1** is the recommended approach. It is more performant and declarative compared to **Option 2**, especially when working with flow triggers dependent on state. +`states` defaults to `[SUCCESS, WARNING]`. Declare it explicitly when you need a different set. diff --git a/src/contents/docs/15.how-to-guides/alerting/index.md b/src/contents/docs/15.how-to-guides/alerting/index.md index b55064a6f8e..8cf269afc9c 100644 --- a/src/contents/docs/15.how-to-guides/alerting/index.md +++ b/src/contents/docs/15.how-to-guides/alerting/index.md @@ -97,7 +97,8 @@ tasks: triggers: - id: on_failure type: io.kestra.plugin.core.trigger.Flow - when: "{{ trigger.executionStatus in ['FAILED', 'WARNING'] }}" + dependsOn: + - states: [FAILED, WARNING] ``` diff --git a/src/contents/docs/15.how-to-guides/multiplecondition-listener/index.md b/src/contents/docs/15.how-to-guides/multiplecondition-listener/index.md index c2336dc3313..4b108ed343a 100644 --- a/src/contents/docs/15.how-to-guides/multiplecondition-listener/index.md +++ b/src/contents/docs/15.how-to-guides/multiplecondition-listener/index.md @@ -8,35 +8,20 @@ topics: - Kestra Workflow Components --- -How to set up a Flow to only trigger when multiple conditions are met. +How to set up a flow that only triggers when multiple upstream flows have all succeeded. -In this tutorial, we’ll explore how to set up a flow in Kestra that only triggers when multiple conditions are met. Specifically, we will create a flow that only executes if two other flows, `multiplecondition-flow-a` and `multiplecondition-flow-b`, have executed successfully within the last 24 hours. +In this guide, we’ll create a flow that only executes if two other flows, `multiplecondition_flow_a` and `multiplecondition_flow_b`, have each completed successfully within the last 24 hours. This pattern uses the `dependsOn` property on the Flow trigger. -## Why Use Multiple Condition Listeners? +## When to use this pattern -The `MultipleCondition` listener allows you to build more complex workflows that depend on the success of several flows. For example, if you have two dependent tasks or processes that need to succeed before triggering another process, this listener ensures that the next workflow is only executed when both conditions are met within a specific time window. +Use multiple upstream dependencies when a downstream process should only run after several independent upstream flows all succeed. For example, if you have separate ingestion flows for different data sources and want to run a transformation only after all sources have completed, `dependsOn` with a time window is the right tool. -## Activation Process Overview +## How it works -The listener will trigger under the following conditions: - -1. Both `multiplecondition-flow-a` and `multiplecondition-flow-b` must have successful executions. -2. The listener checks if both flows succeeded within the last 24 hours. -3. If the conditions are met, the flow is activated, and the conditions reset. -4. Future executions will only re-trigger the flow if both flows succeed again within another 24-hour window. - -## How the Process Works - -1. Time Window (P1D or 24 hours): - - - The `MultipleCondition` listener checks if both flows (`multiplecondition-flow-a` and `multiplecondition-flow-b`) have been executed successfully within the past 24 hours. - -2. Resetting Conditions: - - - Once the listener triggers, the conditions reset, meaning that even if one of the flows succeeds again, the listener won't trigger until both flows succeed within a new 24-hour period. - -3. Flow Dependency: - - This is particularly useful when you have flows that depend on each other or when the successful execution of multiple workflows is a prerequisite for a downstream task. +1. Both `multiplecondition_flow_a` and `multiplecondition_flow_b` must complete successfully. +2. Both must complete within the same 24-hour window (`window.every: P1D`). +3. Once both conditions are satisfied, the listener flow triggers. +4. The window resets each day, so both flows must succeed again within the next window to re-trigger the listener. ## First Flow: `multiplecondition_flow_a` @@ -85,7 +70,7 @@ id: multiplecondition_listener namespace: company.team description: | - This flow will start only if `multiplecondition_flow_a` and `multiplecondition_flow_b` are successful during the last 24h. + This flow starts only if `multiplecondition_flow_a` and `multiplecondition_flow_b` both succeed within the same 24-hour window. tasks: - id: only_listener @@ -95,41 +80,24 @@ tasks: triggers: - id: multiple_listen_flow type: io.kestra.plugin.core.trigger.Flow - when: "{{ trigger.executionStatus == 'SUCCESS' }}" - multipleConditions: - - id: multiple - window: P1D - windowAdvance: P0D - conditions: - flow_a: - type: io.kestra.plugin.core.condition.ExecutionFlow - namespace: company.team - flowId: multiplecondition_flow_a - flow_b: - type: io.kestra.plugin.core.condition.ExecutionFlow - namespace: company.team - flowId: multiplecondition_flow_b + dependsOn: + - flowId: multiplecondition_flow_a + namespace: company.team + states: [SUCCESS] + - flowId: multiplecondition_flow_b + namespace: company.team + states: [SUCCESS] + window: + every: P1D ``` -## Explanation of the Flow +## Explanation of the flow -1. Tasks Section: +1. **Tasks** — `only_listener` outputs a static value when the trigger fires. Replace this with whatever downstream logic you need. +2. **`dependsOn`** — declares two upstream flow dependencies. Both entries must be satisfied before the trigger fires. `states: [SUCCESS]` means only successful executions count. - - The task `only_listener` simply outputs a static value (`children`) when the trigger conditions are met. This part can be customized to perform more complex tasks after the conditions are satisfied. - -2. Triggers Section: - - - - The `multiple_listen_flow` trigger listens for both `multiplecondition_flow_a` and `multiplecondition_flow_b`. - - The `when` expression ensures that only successful executions (status `SUCCESS`) are considered. - - MultipleCondition: This condition checks that both `flow_a` and `flow_b` have successfully completed within the last 24 hours (`P1D`). - -3. Window: - - - - The `window: P1D` ensures that the listener checks for executions within the past 24 hours. - - The `windowAdvance: P0D` parameter ensures that the time window starts immediately, without any delay. +3. **`window.every: P1D`** — defines a 24-hour evaluation window. Kestra accumulates upstream executions within this window and fires the trigger once all `dependsOn` entries are satisfied within the same window period. ## Expected Output @@ -147,6 +115,4 @@ When both multiplecondition_flow_a and multiplecondition_flow_b succeed within 2 ## Conclusion -In this tutorial, we’ve demonstrated how to set up a `MultipleCondition` listener that checks for the success of multiple flows within a specified time window. This is a powerful feature for managing complex workflows that depend on the successful execution of multiple tasks. - -By using this listener, you can ensure that downstream processes are only triggered when all necessary upstream conditions are met. +This guide demonstrated how to use `dependsOn` with a time window to trigger a flow only when multiple upstream flows all succeed within the same period. Use this pattern whenever a downstream process must wait on several independent upstream flows before running. diff --git a/src/contents/docs/15.how-to-guides/secops-with-kestra/index.md b/src/contents/docs/15.how-to-guides/secops-with-kestra/index.md index ae2225f2af3..ea20c73962a 100644 --- a/src/contents/docs/15.how-to-guides/secops-with-kestra/index.md +++ b/src/contents/docs/15.how-to-guides/secops-with-kestra/index.md @@ -223,13 +223,11 @@ triggers: - id: postVMCreation type: io.kestra.plugin.core.trigger.Flow inputs: - ipAddress: "{{ trigger.outputs.externalIPAddress }}" - preconditions: - id: vmCreationSuccess - flows: - - namespace: company.ops.it - flowId: createVMRevamped - states: [ SUCCESS, WARNING ] + ipAddress: "{{ trigger.outputs.createVMRevamped.externalIPAddress }}" + dependsOn: + - namespace: company.ops.it + flowId: createVMRevamped + states: [SUCCESS, WARNING] ``` ## Step 7: Review the Topology diff --git a/src/contents/docs/use-cases/05.approval-processes/index.md b/src/contents/docs/use-cases/05.approval-processes/index.md index b75ccdf90e9..2aff87a48e3 100644 --- a/src/contents/docs/use-cases/05.approval-processes/index.md +++ b/src/contents/docs/use-cases/05.approval-processes/index.md @@ -314,13 +314,10 @@ tasks: triggers: - id: flow type: io.kestra.plugin.core.trigger.Flow - preconditions: - id: flow1 - flows: - - flowId: pause_demo - namespace: demo - states: - - SUCCESS + dependsOn: + - flowId: pause_demo + namespace: demo + states: [SUCCESS] ``` Why this is robust: From 25cc0157595674607d0539aee8f0046407c6dabd Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Wed, 22 Apr 2026 11:11:50 +0200 Subject: [PATCH 27/52] docs(migration-guides): further polish --- .../07.triggers/01.schedule-trigger/index.md | 2 +- .../07.triggers/index.mdx | 4 +- .../v2.0.0/run-if-renamed-when/index.md | 58 +-- .../trigger-conditions-redesign/index.md | 347 ++++++++++-------- 4 files changed, 204 insertions(+), 207 deletions(-) diff --git a/src/contents/docs/05.workflow-components/07.triggers/01.schedule-trigger/index.md b/src/contents/docs/05.workflow-components/07.triggers/01.schedule-trigger/index.md index d39f5d456ac..898286a1ee8 100644 --- a/src/contents/docs/05.workflow-components/07.triggers/01.schedule-trigger/index.md +++ b/src/contents/docs/05.workflow-components/07.triggers/01.schedule-trigger/index.md @@ -93,7 +93,7 @@ You can use this expression to make your **manual execution work**: `{{ trigger. ::: -## Schedule conditions +## Refining schedules with `when` When a `cron` expression alone is not sufficient (e.g., only first Monday of the month, only weekends), you can refine schedules using a `when` Pebble expression. diff --git a/src/contents/docs/05.workflow-components/07.triggers/index.mdx b/src/contents/docs/05.workflow-components/07.triggers/index.mdx index 932932252d7..ee2cf307cc2 100644 --- a/src/contents/docs/05.workflow-components/07.triggers/index.mdx +++ b/src/contents/docs/05.workflow-components/07.triggers/index.mdx @@ -145,8 +145,8 @@ triggers: See the [Flow trigger documentation](./02.flow-trigger/index.md) for the full `dependsOn` and `window` property reference. -:::alert{type="info"} -The old `conditions` list is deprecated in Kestra 2.0 but still functional. Migrate to `when` at your earliest convenience. See the [trigger conditions migration guide](../../11.migration-guide/v2.0.0/trigger-conditions-redesign/index.md) for before/after examples. +:::alert{type="warning"} +The `conditions` list is removed in Kestra 2.0. Flows that still use it will fail to parse after upgrading. See the [trigger conditions migration guide](../../11.migration-guide/v2.0.0/trigger-conditions-redesign/index.md) for before/after examples. ::: ## Unlocking, enabling, and disabling triggers diff --git a/src/contents/docs/11.migration-guide/v2.0.0/run-if-renamed-when/index.md b/src/contents/docs/11.migration-guide/v2.0.0/run-if-renamed-when/index.md index 8aea5ffccf5..02fbabee5bd 100644 --- a/src/contents/docs/11.migration-guide/v2.0.0/run-if-renamed-when/index.md +++ b/src/contents/docs/11.migration-guide/v2.0.0/run-if-renamed-when/index.md @@ -4,21 +4,18 @@ sidebarTitle: runIf → when (Tasks) icon: /src/contents/docs/icons/migration-guide.svg release: 2.0.0 editions: ["OSS", "EE"] -description: The task-level runIf property has been renamed to when in Kestra 2.0.0, aligning it with the when property introduced on triggers. +description: The task-level runIf property is renamed to when in Kestra 2.0, aligning it with the when property introduced on all triggers. --- -Kestra 2.0.0 unifies conditional execution under a single property name: `when`. +Kestra 2.0 introduces a `when` property on all triggers to replace the `conditions` list. To unify conditional execution under a single property name, the task-level `runIf` is renamed to `when` in the same release. -- **Tasks** — `runIf` is renamed to `when`. -- **Triggers** — the `conditions` list is deprecated in favor of a new `when` Pebble expression string. +No behavioral change. The Pebble expression is rendered at runtime and the task is set to `SKIPPED` if the result is falsy (`false`, `0`, `-0`, or an empty string) — identical to the existing `runIf` behavior. -Both properties behave the same way: the Pebble expression is rendered at runtime, and if the result is falsy (`false`, `0`, `-0`, or an empty string), the task is set to `SKIPPED` or the trigger does not fire. - -A deprecated alias keeps `runIf` functional in 2.0.0 so existing flows continue to parse without changes. The alias is scheduled for removal in a future version — update your flows now to avoid a hard break later. - -## Tasks: runIf → when +:::alert{type="warning"} +`runIf` is kept as a deprecated alias in 2.0 so existing flows continue to parse. The alias will be removed in a future release. Update your flows now to avoid a hard break later. +::: -### Before +## Before ```yaml tasks: @@ -28,7 +25,7 @@ tasks: runIf: "{{ inputs.run_task }}" ``` -### After +## After ```yaml tasks: @@ -38,44 +35,9 @@ tasks: when: "{{ inputs.run_task }}" ``` -The behavior is identical — the same Pebble rendering and `SKIPPED` state logic apply. - -## Triggers: conditions → dependsOn (Flow triggers) - -The `conditions` list on Flow triggers is replaced by `dependsOn`. State filtering moves to the `states` property on each entry. - -### Before - -```yaml -triggers: - - id: on_success - type: io.kestra.plugin.core.trigger.Flow - conditions: - - type: io.kestra.plugin.core.condition.ExecutionStatus - in: - - SUCCESS -``` - -### After - -```yaml -triggers: - - id: on_success - type: io.kestra.plugin.core.trigger.Flow - dependsOn: - - flowId: upstream_flow - namespace: company.team - states: [SUCCESS] -``` - -For the full before/after reference for Flow trigger conditions, including namespace filtering, label matching, and `preconditions` migration, see the [trigger conditions redesign guide](../trigger-conditions-redesign/index.md). - ## Migration steps 1. **Search your flows** for `runIf:` and replace each occurrence with `when:`. The property value and any Pebble expressions stay the same. -2. **Search your flows** for `conditions:` on Flow trigger blocks and replace them with `dependsOn` entries. See the [trigger conditions redesign guide](../trigger-conditions-redesign/index.md) for before/after examples. -3. **Validate** by saving the updated flows in the Kestra UI or via the API. +2. **Validate** by saving the updated flows in the Kestra UI or via the API. -:::alert{type="warning"} -The `runIf` alias will be removed in a future release. Flows that still use `runIf` will fail to parse after the alias is dropped. -::: +For trigger condition changes (`conditions` → `when` on Schedule and Webhook triggers, `conditions`/`preconditions` → `dependsOn` on Flow triggers), see the [trigger conditions redesign guide](../trigger-conditions-redesign/index.md). diff --git a/src/contents/docs/11.migration-guide/v2.0.0/trigger-conditions-redesign/index.md b/src/contents/docs/11.migration-guide/v2.0.0/trigger-conditions-redesign/index.md index 55a9c47b898..dcc6990cc41 100644 --- a/src/contents/docs/11.migration-guide/v2.0.0/trigger-conditions-redesign/index.md +++ b/src/contents/docs/11.migration-guide/v2.0.0/trigger-conditions-redesign/index.md @@ -4,36 +4,35 @@ sidebarTitle: Trigger Conditions Redesign icon: /src/contents/docs/icons/migration-guide.svg release: 2.0.0 editions: ["OSS", "EE"] -description: The conditions list on triggers and the preconditions block on Flow triggers are removed in Kestra 2.0. Schedule and Webhook triggers use a top-level when expression. Flow triggers use dependsOn and window. +description: The conditions list on triggers and the preconditions block on Flow triggers are removed in Kestra 2.0. All trigger types use a top-level when Pebble expression. Flow triggers also use dependsOn and window. --- Kestra 2.0 replaces the `conditions` and `preconditions` system across all trigger types. -- **Schedule and Webhook triggers** — the `conditions` list is removed in favor of a top-level `when` Pebble expression. +- **All trigger types (Schedule, Webhook, HTTP, Flow, and others)** — the `conditions` list is removed in favor of a top-level `when` Pebble expression. - **Flow triggers** — both `conditions` and `preconditions` are removed in favor of `dependsOn` (upstream flow entries) and `window` (time window configuration). - **Flow trigger outputs** — scoped by flow ID: `trigger.outputs..`. - **Input rendering failures** — now create a `FAILED` execution instead of silently dropping the event. Both `conditions` and `preconditions` are removed in Kestra 2.0. Flows that still use them will fail to parse after upgrading. -## `conditions` → `when` on Schedule and Webhook triggers +## `conditions` → `when` on all triggers -### What replaces what +All trigger types gain a top-level `when` property containing a Pebble expression. When the expression evaluates to `true`, the trigger fires; when `false`, it is skipped. This replaces the `conditions` list, which required a fully qualified Java type for every filtering need and did not compose cleanly across trigger types. -| Old condition type | New `when` expression | +### `when` expression context + +The variables available in a `when` expression depend on the trigger type: + +| Trigger type | Available variables | |---|---| -| `DayWeek` (e.g. MONDAY) | `{{ dayOfWeek(trigger.date) == 'MONDAY' }}` | -| `Weekend` | `{{ isWeekend(trigger.date) }}` | -| `Not` > `Weekend` (weekdays only) | `{{ not isWeekend(trigger.date) }}` | -| `Not` > `DayWeek` SUNDAY (exclude Sundays) | `{{ dayOfWeek(trigger.date) != 'SUNDAY' }}` | -| `PublicHoliday` (country: FR) | `{{ isPublicHoliday(trigger.date, 'FR') }}` | -| `Not` > `PublicHoliday` + `Weekend` (workdays) | `{{ not isWeekend(trigger.date) and not isPublicHoliday(trigger.date, 'FR') }}` | -| `DayWeekInMonth` (MONDAY, FIRST) | `{{ isDayWeekInMonth(trigger.date, 'MONDAY', 'FIRST') }}` | -| `DateTimeBetween` (after/before) | `{{ trigger.date > '2025-12-31T23:59:59Z' and trigger.date < '2026-06-30T23:59:59Z' }}` | -| `TimeBetween` (08:00-17:00) | `{{ hourOfDay(trigger.date) >= 8 and hourOfDay(trigger.date) < 17 }}` | -| `Expression` (custom Pebble) | Direct `when` expression, no wrapper needed | -| `Expression` on webhook body/headers | `{{ trigger.body.field == 'value' }}` or `{{ trigger.headers['X-Key'] == 'value' }}` | -| Multiple `Expression` conditions | Combined with `and` / `or` in a single `when` | +| Schedule | `trigger.date`, `trigger.timestamp` | +| Webhook | `trigger.body`, `trigger.headers` | +| Flow | `namespace`, `flowId`, `state`, `labels`, `outputs`, `hasRetryAttempt` | + +:::alert{type="info"} +**Schedule date skipping:** When a Schedule trigger has a `when` expression, the scheduler evaluates it against each candidate date. If `when` evaluates to `false`, the scheduler skips that date and advances to the next cron-matching date. This is the same behavior as the previous `conditions` on Schedule triggers — `when` controls which scheduled dates fire, not just whether a single date fires. +::: ### New Pebble helper functions @@ -358,37 +357,32 @@ triggers: Multiple `Expression` conditions combine into a single `when` expression using `and` / `or`. -For the full list of Pebble calendar helper functions (`isWeekend`, `isPublicHoliday`, `isDayWeekInMonth`, `hourOfDay`, etc.), see the [date and calendar helpers](../../../expressions/index.mdx#date-and-calendar-helpers) reference. +### What replaces what -## `conditions` and `preconditions` → `dependsOn` on Flow triggers +| Old condition type | New `when` expression | +|---|---| +| `DayWeek` (e.g. MONDAY) | `{{ dayOfWeek(trigger.date) == 'MONDAY' }}` | +| `Weekend` | `{{ isWeekend(trigger.date) }}` | +| `Not` > `Weekend` (weekdays only) | `{{ not isWeekend(trigger.date) }}` | +| `Not` > `DayWeek` SUNDAY (exclude Sundays) | `{{ dayOfWeek(trigger.date) != 'SUNDAY' }}` | +| `PublicHoliday` (country: FR) | `{{ isPublicHoliday(trigger.date, 'FR') }}` | +| `Not` > `PublicHoliday` + `Weekend` (workdays) | `{{ not isWeekend(trigger.date) and not isPublicHoliday(trigger.date, 'FR') }}` | +| `DayWeekInMonth` (MONDAY, FIRST) | `{{ isDayWeekInMonth(trigger.date, 'MONDAY', 'FIRST') }}` | +| `DateTimeBetween` (after/before) | `{{ trigger.date > '2025-12-31T23:59:59Z' and trigger.date < '2026-06-30T23:59:59Z' }}` | +| `TimeBetween` (08:00-17:00) | `{{ hourOfDay(trigger.date) >= 8 and hourOfDay(trigger.date) < 17 }}` | +| `Expression` (custom Pebble) | Direct `when` expression, no wrapper needed | +| `Expression` on webhook body/headers | `{{ trigger.body.field == 'value' }}` or `{{ trigger.headers['X-Key'] == 'value' }}` | +| Multiple `Expression` conditions | Combined with `and` / `or` in a single `when` | -Both `conditions` (execution-level condition types such as `ExecutionStatus`, `ExecutionFlow`, `ExecutionNamespace`) and `preconditions` (upstream flow lists with time windows) are replaced by a single `dependsOn` list. Each entry declares one upstream dependency with typed properties. +For the full list of Pebble calendar helper functions (`isWeekend`, `isPublicHoliday`, `isDayWeekInMonth`, `hourOfDay`, etc.), see the [date and calendar helpers](../../../expressions/index.mdx#date-and-calendar-helpers) reference. -### What replaces what +## `conditions` and `preconditions` → `dependsOn` on Flow triggers -| Old property / condition type | New equivalent | -|---|---| -| `conditions` list on Flow trigger | `dependsOn` list | -| `preconditions` block | `dependsOn` list + `window` | -| `multipleConditions` block | `dependsOn` list + `window.every` | -| `ExecutionStatus` (`in: [SUCCESS]`) | `states: [SUCCESS]` on the `dependsOn` entry | -| `ExecutionFlow` (`flowId`, `namespace`) | `flowId` + `namespace` on the `dependsOn` entry | -| `ExecutionNamespace` (exact) | `namespace` on the `dependsOn` entry | -| `ExecutionNamespace` (`comparison: PREFIX`) | `when: "{{ namespace \| startsWith('...') }}"` on the entry | -| `ExecutionLabels` (`labels: {k: v}`) | `labels: {k: v}` on the `dependsOn` entry | -| `ExecutionOutputs` (`expression`) | `when` with `outputs.` on the entry | -| `HasRetryAttempt` | `when: "{{ hasRetryAttempt == true }}"` on the entry | -| `Not` > `ExecutionStatus` | Explicit `states` list or `when: "{{ state != 'SUCCESS' }}"` | -| Multiple triggers for OR logic | `mode: ANY` with `dependsOn` entries | -| `preconditions.resetOnSuccess: true` | `window.fireOnce: true` | -| `timeWindow.type: DAILY_TIME_DEADLINE` | `window.deadline` | -| `timeWindow.type: DAILY_TIME_WINDOW` | `window.from` + `window.to` | -| `timeWindow.type: DURATION_WINDOW` | `window.every` | -| `timeWindow.type: SLIDING_WINDOW` | `window.lookback` | +Both `conditions` (execution-level types such as `ExecutionStatus`, `ExecutionFlow`, `ExecutionNamespace`) and `preconditions` (upstream flow lists with time windows) are replaced by a single `dependsOn` list. Each entry declares one upstream dependency with typed properties. ### `dependsOn` entry properties -| `dependsOn` entry property | Type | Default | Description | +| Property | Type | Default | Description | |---|---|---|---| | `flowId` | string | — | Exact flow ID to match. Omit to match any flow. | | `namespace` | string | — | Exact namespace to match. Use `when` for prefix or pattern matching. | @@ -396,13 +390,17 @@ Both `conditions` (execution-level condition types such as `ExecutionStatus`, `E | `labels` | map | — | Labels the upstream execution must carry (all must match). | | `when` | string | — | Pebble expression for additional filtering on the upstream execution context. | +Both `flowId` and `namespace` use exact matching: `namespace: company.team` matches only `company.team`, not `company.team.project`. For prefix or pattern matching, use `when` with `startsWith` or `endsWith`. + :::alert{type="warning"} The default `states` changed from `[SUCCESS, WARNING, PAUSED]` to `[SUCCESS, WARNING]`. If your flows relied on `PAUSED` being included by default, add it explicitly: `states: [SUCCESS, WARNING, PAUSED]`. ::: -### Single upstream flow (from `preconditions`) +### Single upstream flow -**Before** +The `preconditions` block and the `conditions`-based approach both map to a single `dependsOn` entry. + +**Before (from `preconditions`)** ```yaml triggers: @@ -416,21 +414,7 @@ triggers: states: [SUCCESS] ``` -**After** - -```yaml -triggers: - - id: after_extract - type: io.kestra.plugin.core.trigger.Flow - dependsOn: - - flowId: extract - namespace: company.team - states: [SUCCESS] -``` - -### Single upstream flow (from `conditions`) - -**Before** +**Before (from `conditions`)** ```yaml triggers: @@ -447,7 +431,7 @@ triggers: ```yaml triggers: - - id: on_completion + - id: after_extract type: io.kestra.plugin.core.trigger.Flow dependsOn: - flowId: extract @@ -531,10 +515,10 @@ triggers: namespace: company.team states: [SUCCESS] window: - every: P1D + every: PT1D ``` -The arbitrary string keys (`flow_a`, `flow_b`) are dropped. `dependsOn` is always a list. +The arbitrary string keys (`flow_a`, `flow_b`) are dropped — `dependsOn` is always a list. The `windowAdvance` property is removed with no direct equivalent. ### Namespace-wide alerting (prefix matching) @@ -765,7 +749,99 @@ triggers: Same `dependsOn` syntax regardless of whether the original used `conditions` or `preconditions`. -### OR logic: fire when any upstream completes +### Passing outputs downstream + +Flow trigger outputs are now scoped by flow ID. The path format is `trigger.outputs..`. + +**Before** (flat map — all upstream outputs merged together) + +```yaml +triggers: + - id: after_extract + type: io.kestra.plugin.core.trigger.Flow + inputs: + date: "{{ trigger.outputs.date }}" + preconditions: + id: flows + flows: + - namespace: company.team + flowId: extract + states: [SUCCESS] +``` + +**After** (scoped by flow ID) + +```yaml +triggers: + - id: after_extract + type: io.kestra.plugin.core.trigger.Flow + inputs: + date: "{{ trigger.outputs.extract.date }}" + dependsOn: + - flowId: extract + namespace: company.team +``` + +For multi-flow triggers, each upstream flow's outputs are accessed under its own key: + +```yaml +dependsOn: + - flowId: stg_sales + namespace: company.team + - flowId: stg_marketing + namespace: company.team +``` + +Access as `{{ trigger.outputs.stg_sales.row_count }}` and `{{ trigger.outputs.stg_marketing.row_count }}`. + +:::alert{type="warning"} +**Breaking change for multi-flow triggers.** Update all `trigger.outputs.` references to `trigger.outputs..`. For triggers with a single `dependsOn` entry, the unscoped form `{{ trigger.outputs. }}` still works as a shorthand — no update required. +::: + +#### ForEachItem chain + +When using Flow triggers to chain `ForEachItem` child flows, reference the child flow's outputs using its `flowId`: + +**Before** + +```yaml +triggers: + - id: 01_complete + type: io.kestra.plugin.core.trigger.Flow + inputs: + testFile: "{{ trigger.outputs.myFile }}" + preconditions: + id: output_01_success + flows: + - namespace: io.kestra.tests.trigger.foreachitem + flowId: flow-trigger-for-each-item-child + states: [SUCCESS] +``` + +**After** + +```yaml +triggers: + - id: 01_complete + type: io.kestra.plugin.core.trigger.Flow + inputs: + testFile: "{{ trigger.outputs.flow-trigger-for-each-item-child.myFile }}" + dependsOn: + - flowId: flow-trigger-for-each-item-child + namespace: io.kestra.tests.trigger.foreachitem +``` + +### `mode`: OR and N-of-M logic + +The `mode` property controls how `dependsOn` entries are combined when evaluating whether to fire. + +| Value | Behavior | Required properties | +|---|---|---| +| `ALL` (default) | Fires when all `dependsOn` entries are satisfied | — | +| `ANY` | Fires as soon as any one entry is satisfied | — | +| `AT_LEAST` | Fires when at least `minSatisfied` entries are satisfied | `minSatisfied` (integer ≥ 1, ≤ entry count) | + +#### OR logic: fire when any upstream completes Previously, OR logic required N separate Flow triggers. `mode: ANY` consolidates them into one. @@ -809,15 +885,7 @@ triggers: `mode: ANY` fires as soon as either dependency is satisfied. The default `mode: ALL` requires every entry to be satisfied before the trigger fires. -### `mode` values - -| Value | Behavior | Required properties | -|---|---|---| -| `ALL` (default) | Fires when all `dependsOn` entries are satisfied | — | -| `ANY` | Fires as soon as any one entry is satisfied | — | -| `AT_LEAST` | Fires when at least `minSatisfied` entries are satisfied | `minSatisfied` (integer ≥ 1, ≤ entry count) | - -### OR logic with a time window +#### OR logic with a time window ```yaml triggers: @@ -835,7 +903,7 @@ triggers: Fire before 9 AM when either source completes. -### N of M: at least 2 out of 3 +#### N of M: at least 2 out of 3 ```yaml triggers: @@ -857,65 +925,45 @@ triggers: deadline: "09:00:00" ``` -`mode: AT_LEAST` fires when `minSatisfied` entries are satisfied. `minSatisfied` must be >= 1 and <= the number of `dependsOn` entries. - -## Scoped trigger outputs - -Flow trigger outputs are scoped by flow ID to prevent key collisions when multiple upstream flows are involved. - -**Before** (flat map — all upstream outputs merged together) - -```yaml -triggers: - - id: after_extract - type: io.kestra.plugin.core.trigger.Flow - inputs: - date: "{{ trigger.outputs.date }}" - preconditions: - id: flows - flows: - - namespace: company.team - flowId: extract - states: [SUCCESS] -``` - -**After** (scoped by flow ID) +`mode: AT_LEAST` fires when `minSatisfied` entries are satisfied. `minSatisfied` must be ≥ 1 and ≤ the number of `dependsOn` entries. -```yaml -triggers: - - id: after_extract - type: io.kestra.plugin.core.trigger.Flow - inputs: - date: "{{ trigger.outputs.extract.date }}" - dependsOn: - - flowId: extract - namespace: company.team -``` - -The path format is `trigger.outputs..`. For multi-flow triggers, each upstream flow's outputs are accessed separately: +### What replaces what -```yaml -dependsOn: - - flowId: stg_sales - namespace: company.team - - flowId: stg_marketing - namespace: company.team -``` +| Old property / condition type | New equivalent | +|---|---| +| `conditions` list on Flow trigger | `dependsOn` list | +| `preconditions` block | `dependsOn` list + `window` | +| `multipleConditions` block | `dependsOn` list + `window.every` | +| `ExecutionStatus` (`in: [SUCCESS]`) | `states: [SUCCESS]` on the `dependsOn` entry | +| `ExecutionFlow` (`flowId`, `namespace`) | `flowId` + `namespace` on the `dependsOn` entry | +| `ExecutionNamespace` (exact) | `namespace` on the `dependsOn` entry | +| `ExecutionNamespace` (`comparison: PREFIX`) | `when: "{{ namespace \| startsWith('...') }}"` on the entry | +| `ExecutionLabels` (`labels: {k: v}`) | `labels: {k: v}` on the `dependsOn` entry | +| `ExecutionOutputs` (`expression`) | `when` with `outputs.` on the entry | +| `HasRetryAttempt` | `when: "{{ hasRetryAttempt == true }}"` on the entry | +| `Not` > `ExecutionStatus` | Explicit `states` list or `when: "{{ state != 'SUCCESS' }}"` | +| Multiple triggers for OR logic | `mode: ANY` with `dependsOn` entries | +| `preconditions.resetOnSuccess: true` | `window.fireOnce: true` | +| `timeWindow.type: DAILY_TIME_DEADLINE` | `window.deadline` | +| `timeWindow.type: DAILY_TIME_WINDOW` | `window.from` + `window.to` | +| `timeWindow.type: DURATION_WINDOW` | `window.every` | +| `timeWindow.type: SLIDING_WINDOW` | `window.lookback` | -Access as `{{ trigger.outputs.stg_sales.row_count }}` and `{{ trigger.outputs.stg_marketing.row_count }}`. +## Window configuration -:::alert{type="warning"} -This is a breaking change. Update all `trigger.outputs.` references to `trigger.outputs..` when migrating to Kestra 2.0. -::: +The `window` property applies to Flow triggers and controls how Kestra accumulates upstream executions before evaluating `dependsOn` entries. Set exactly one property group per window; combining groups is a validation error. -## Window configuration +| Window type | Properties | Behavior | +|---|---|---| +| Deadline | `deadline: "09:00:00+01:00"` | Upstream flows must complete by a fixed time each day | +| Daily time range | `from: "06:00:00"` + `to: "12:00:00"` | Only executions within a daily time range count | +| Fixed interval | `every: PT1D` + optional `offset: PT6H` | Recurring window of a fixed size, offset from midnight | +| Lookback | `lookback: PT1H` | Rolling window looking back from the current evaluation time | -The `window` property controls how Kestra accumulates upstream executions before evaluating `dependsOn` entries. Set exactly one group of properties per window; combining groups is a validation error. +`fireOnce: true` can be added to any window type to limit the trigger to firing once per window period rather than every time conditions are met. ### Deadline -All upstream flows must complete before a fixed time each day: - ```yaml window: deadline: "09:00:00+01:00" @@ -923,8 +971,6 @@ window: ### Daily time range -Only executions that completed within a specific time range each day count: - ```yaml window: from: "06:00:00" @@ -933,8 +979,6 @@ window: ### Fixed interval -Recurring window of a fixed size, with an optional offset from midnight: - ```yaml window: every: PT1D @@ -943,8 +987,6 @@ window: ### Lookback -Count executions that completed within the past duration, relative to the current evaluation time: - ```yaml window: lookback: PT1H @@ -952,8 +994,6 @@ window: ### Fire once per window -Set `fireOnce: true` to ensure the trigger fires at most once per window period, even if conditions are satisfied multiple times within it: - ```yaml window: deadline: "09:00:00+01:00" @@ -964,24 +1004,14 @@ Default is `false` — the trigger fires every time conditions are met within th ### SLA misses with `onMiss` -Declare what happens when the deadline passes without all dependencies being satisfied: +`onMiss` is a trigger-level property (peer to `window`) that declares what happens when the deadline passes without all dependencies being satisfied: ```yaml -triggers: - - id: after_staging - type: io.kestra.plugin.core.trigger.Flow - dependsOn: - - flowId: stg_sales - namespace: company.team - - flowId: stg_marketing - namespace: company.team - window: - deadline: "09:00:00+01:00" - onMiss: - behavior: FAIL - labels: - sla: miss - reason: upstreamNotFinishedOnTime +onMiss: + behavior: FAIL + labels: + sla: miss + reason: upstreamNotFinishedOnTime ``` `behavior: FAIL` creates a `FAILED` execution when the deadline passes. Labels are applied to that execution for downstream alerting. @@ -997,22 +1027,27 @@ triggers: `preconditions.resetOnSuccess: true` maps to `window.fireOnce: true`. -## Silent failures → FAILED executions +## Behavior changes after upgrading + +### Silent failures → FAILED executions + +Previously, if an expression on a Flow trigger failed to render (for example, because an upstream output key did not exist), the trigger silently dropped the event and no execution was created. In Kestra 2.0, a `FAILED` execution is created instead, making failures visible in the UI and actionable via downstream alerting. -Previously, if the `inputs` expression on a Flow trigger failed to render (for example, because an upstream output key did not exist), the trigger silently dropped the event and no execution was created. In Kestra 2.0, a `FAILED` execution is created instead. This makes failures visible in the Kestra UI and allows you to configure downstream alerting. +No migration action is required. Review your Flow trigger `inputs` expressions to ensure they reference valid output keys and avoid unexpected `FAILED` executions after upgrading. -No migration action is required. Review your Flow trigger `inputs` expressions and ensure they reference valid output keys to avoid unexpected `FAILED` executions after upgrading. +### State store reset and in-flight events -## Stable condition IDs +Previously, auto-generated condition keys (`condition_1`, `condition_2`, …) meant that reordering entries could reset accumulated window state. In Kestra 2.0, `dependsOn` entry keys are derived from each entry's `namespace` and `flowId`, making them order-independent. -`dependsOn` condition IDs are derived from a stable hash of each entry's `namespace`, `flowId`, `when`, `states`, and `labels`. Previously, condition IDs were auto-incremented (`condition_1`, `condition_2`, …) and reordering entries would reset the accumulated window state. +The trigger-level state store key also changes: the old scheme used `preconditions.id`; the new scheme uses `{flowId}/{triggerId}`. Existing accumulated state from `preconditions` will not be found after upgrading — in-flight multi-flow triggers re-evaluate from scratch. For most deployments this means at most one missed trigger cycle. -Existing state store entries from `preconditions` use `preconditions.id` as their key. After upgrading, in-flight multi-flow triggers re-evaluate from scratch. For most deployments this means at most one missed trigger cycle. +Old-format events in the async queue are discarded gracefully (logged as a warning). No user action is required. ## Migration steps -1. **Replace `conditions:` on Schedule and Webhook triggers** with a `when:` Pebble expression using the before/after examples above as a reference. +1. **Replace `conditions:` on all triggers** with a `when:` Pebble expression. This applies to Schedule, Webhook, HTTP, and any other trigger type that used `conditions`. 2. **Replace `conditions:` and `preconditions:` on Flow triggers** with `dependsOn:` entries and (if applicable) `window:`. -3. **Update `trigger.outputs` references** from `trigger.outputs.` to `trigger.outputs..`. -4. **Update `timeWindow` to `window`** using the property mapping table above. -5. **Validate** by saving the updated flows in the Kestra UI or via the API and confirming that they parse without errors. +3. **Check for `PAUSED` state dependencies.** The default `states` changed from `[SUCCESS, WARNING, PAUSED]` to `[SUCCESS, WARNING]`. Add `PAUSED` explicitly if your flows depended on it: `states: [SUCCESS, WARNING, PAUSED]`. +4. **Update `trigger.outputs` references** in multi-flow triggers from `trigger.outputs.` to `trigger.outputs..`. Single-flow triggers can keep the unscoped form. +5. **Update `timeWindow` to `window`** using the property mapping table above. +6. **Validate** by saving updated flows in the Kestra UI or via the API and confirming they parse without errors. From 57e97c1c8feea02649eb5ec89009d39c05b30b5c Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Wed, 22 Apr 2026 16:57:25 +0200 Subject: [PATCH 28/52] docs(worker-groups): dynamic resolve to null Part of https://github.com/kestra-io/kestra-ee/issues/6629 --- .../04.scalability/worker-group/index.md | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/contents/docs/07.enterprise/04.scalability/worker-group/index.md b/src/contents/docs/07.enterprise/04.scalability/worker-group/index.md index a961bcde40d..aebfa6d767f 100644 --- a/src/contents/docs/07.enterprise/04.scalability/worker-group/index.md +++ b/src/contents/docs/07.enterprise/04.scalability/worker-group/index.md @@ -88,6 +88,27 @@ tasks: key: "{{ inputs.my_worker_group }}" ``` +If the expression resolves to `null` or a blank string, the task is routed to the default worker group — the same behavior as omitting `workerGroup` entirely. This makes `null` a useful sentinel for conditional routing: + +```yaml +id: worker_group_conditional +namespace: company.team + +inputs: + - id: use_gpu + type: BOOLEAN + defaults: false + +tasks: + - id: train + type: io.kestra.plugin.core.debug.Return + format: "{{ taskrun.startDate }}" + workerGroup: + key: "{{ inputs.use_gpu ? 'gpu' : null }}" +``` + +When `inputs.use_gpu` is `false`, the key resolves to `null` and the task runs on the default worker group. When `true`, it targets the `gpu` worker group. + ## Worker Group fallback behavior :::badge{version=">=0.20" editions="EE"} From aad45e71e3fc234ed018d9d11aa19803c00252cf Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Fri, 24 Apr 2026 12:50:58 +0200 Subject: [PATCH 29/52] docs(migration-guide): ForEach^ to Loop --- .../01.tasks/00.flowable-tasks/index.md | 93 ++++- .../v2.0.0/foreach-loop/index.md | 350 ++++++++++++++++++ 2 files changed, 433 insertions(+), 10 deletions(-) create mode 100644 src/contents/docs/11.migration-guide/v2.0.0/foreach-loop/index.md diff --git a/src/contents/docs/05.workflow-components/01.tasks/00.flowable-tasks/index.md b/src/contents/docs/05.workflow-components/01.tasks/00.flowable-tasks/index.md index 88e940be42c..9069795ab18 100644 --- a/src/contents/docs/05.workflow-components/01.tasks/00.flowable-tasks/index.md +++ b/src/contents/docs/05.workflow-components/01.tasks/00.flowable-tasks/index.md @@ -142,6 +142,10 @@ For more details, check out the [If Task documentation](/plugins/core/flow/io.ke ### ForEach +:::alert{type="warning"} +`ForEach` is removed in Kestra 2.0. Use the [`Loop` task](#loop) instead. See the [ForEach → Loop migration guide](../../../11.migration-guide/v2.0.0/foreach-loop/index.md). +::: + This task executes a group of tasks for each value in the list. In the following example, the variable is static, but it could also be generated from a previous task output, starting any number of subtasks. @@ -209,6 +213,10 @@ For more details, refer to the [ForEach Task documentation](/plugins/core/flow/i ### ForEachItem +:::alert{type="warning"} +`ForEachItem` is removed in Kestra 2.0. Use the [`Loop` task](#loop) with a URI value instead. See the [ForEach → Loop migration guide](../../../11.migration-guide/v2.0.0/foreach-loop/index.md). +::: + This task iterates over a list of items and runs a subflow for each item, or for each batch of items. ```yaml @@ -286,7 +294,7 @@ The `Loop` task iterates over a set of values and runs a set of child tasks for `values` accepts a list, a JSON array string, a map, or an ION file URI. When `values` is a URI, Kestra performs one iteration per line of the file. ```yaml -id: loop_example +id: loop-basic namespace: company.team tasks: @@ -294,9 +302,9 @@ tasks: type: io.kestra.plugin.core.flow.Loop values: ["value 1", "value 2", "value 3"] tasks: - - id: process + - id: log type: io.kestra.plugin.core.log.Log - message: "{{ item.index }} - {{ item.value }}" + message: "index={{ item.index }} value={{ item.value }}" ``` Inside each iteration, use the `item` variable to access the iteration context: @@ -321,11 +329,11 @@ tasks: - id: loop type: io.kestra.plugin.core.flow.Loop values: [1, 2, 3, 4, 5] - concurrencyLimit: 2 + concurrencyLimit: 3 tasks: - - id: process + - id: log type: io.kestra.plugin.core.log.Log - message: "Processing {{ item.value }}" + message: "Processing {{ item.value }} (index={{ item.index }})" ``` #### Failure propagation @@ -336,11 +344,19 @@ By default (`transmitFailed: true`), a failed iteration causes the Loop task its tasks: - id: loop type: io.kestra.plugin.core.flow.Loop - values: ["value 1", "value 2", "value 3"] + values: ["ok", "fail", "ok"] transmitFailed: false tasks: - - id: attempt - type: io.kestra.plugin.core.execution.Fail + - id: maybe_fail + type: io.kestra.plugin.core.flow.If + condition: '{{ item.value == "fail" }}' + then: + - id: do_fail + type: io.kestra.plugin.core.execution.Fail + else: + - id: success + type: io.kestra.plugin.core.log.Log + message: "OK: {{ item.value }}" ``` #### Nested loops @@ -355,7 +371,7 @@ tasks: tasks: - id: inner type: io.kestra.plugin.core.flow.Loop - values: [1, 2, 3] + values: ["a", "b"] tasks: - id: log type: io.kestra.plugin.core.log.Log @@ -369,6 +385,9 @@ For deeper hierarchies, `item.parents[0]` is the immediate parent loop, `item.pa By default, task outputs produced inside a loop are not accessible to tasks that run after the loop. Use the `outputs` property on the Loop task to explicitly declare which values to expose. ```yaml +id: loop-outputs +namespace: company.team + tasks: - id: loop type: io.kestra.plugin.core.flow.Loop @@ -381,6 +400,10 @@ tasks: - id: process type: io.kestra.plugin.core.debug.Return format: "processed {{ item.value }}" + + - id: summary + type: io.kestra.plugin.core.log.Log + message: "Loop ran {{ outputs.loop.iterationCount }} iterations" ``` The loop also exposes monitoring outputs regardless of whether `outputs` is declared: @@ -393,6 +416,56 @@ The loop also exposes monitoring outputs regardless of whether `outputs` is decl The `fetchType` property controls how iteration outputs are collected: `FETCH` returns them directly in the execution context, `STORE` writes them to internal storage as a URI, and `AUTO` (the default) chooses based on whether `values` is a URI. +#### Accessing loop outputs in a script task + +The following example runs a Python task inside a loop to compute a value, then reads the collected results in a subsequent Python task using the monitoring output and the Kestra Python SDK. + +```yaml +id: loop-python-outputs +namespace: company.team + +tasks: + - id: process_items + type: io.kestra.plugin.core.flow.Loop + values: [1, 2, 3, 4, 5] + outputs: + - id: squared + type: INT + value: "{{ outputs.compute.vars.result }}" + tasks: + - id: compute + type: io.kestra.plugin.scripts.python.Script + dependencies: + - kestra + script: | + from kestra import Kestra + n = {{ item.value }} + Kestra.outputs({"result": n * n}) + + - id: analyze + type: io.kestra.plugin.scripts.python.Script + dependencies: + - kestra + script: | + from kestra import Kestra + + iteration_count = {{ outputs.process_items.iterationCount }} + + # outputs.process_items.outputs is a map keyed by iteration value string: + # {"1": {"squared": 1}, "2": {"squared": 4}, "3": {"squared": 9}, ...} + all_outputs = {{ outputs.process_items.outputs | toJson }} + + squared_values = [v["squared"] for v in all_outputs.values()] + + print(f"Processed {iteration_count} items") + print(f"Squared values: {squared_values}") + print(f"Sum of squares: {sum(squared_values)}") + + Kestra.outputs({"total": sum(squared_values)}) +``` + +`outputs.process_items.iterationCount` is always available after the loop finishes. `outputs.process_items.outputs` is a map keyed by iteration value string — for `values: [1, 2, 3, 4, 5]`, the keys are `"1"`, `"2"`, `"3"`, `"4"`, `"5"`. To access a single iteration's output directly in an expression, use `outputs.process_items.outputs['1'].squared`. + For more details, see the [Loop task documentation](/plugins/core/flow/io.kestra.plugin.core.flow.loop). --- diff --git a/src/contents/docs/11.migration-guide/v2.0.0/foreach-loop/index.md b/src/contents/docs/11.migration-guide/v2.0.0/foreach-loop/index.md new file mode 100644 index 00000000000..4f60b7a9117 --- /dev/null +++ b/src/contents/docs/11.migration-guide/v2.0.0/foreach-loop/index.md @@ -0,0 +1,350 @@ +--- +title: ForEach and ForEachItem Replaced by Loop +sidebarTitle: ForEach Replaced by Loop +icon: /src/contents/docs/icons/migration-guide.svg +release: 2.0.0 +editions: ["OSS", "EE"] +description: ForEach and ForEachItem are removed in Kestra 2.0. The Loop task replaces both with isolated sub-executions per iteration, safer expression syntax, and built-in output collection. +--- + +`ForEach` and `ForEachItem` are removed in Kestra 2.0. The `Loop` task replaces both. + +:::alert{type="warning"} +Flows that still reference `io.kestra.plugin.core.flow.ForEach` or `io.kestra.plugin.core.flow.ForEachItem` will fail to parse after upgrading to 2.0.0. Complete this migration before upgrading. +::: + +This guide covers both migrations. If your flows use `ForEach`, follow the [Migrating `ForEach`](#migrating-foreach) section. If your flows use `ForEachItem`, follow the [Migrating `ForEachItem`](#migrating-foreachitem) section. Both must be complete before you upgrade. + +## Why the change + +`ForEach` ran all iterations as task runs inside the **same execution**. A flow iterating over a large dataset — thousands of files, rows, or API results — could generate tens of thousands of task runs in a single execution. That volume would exhaust executor memory and could bring down the entire Kestra instance. The failure was not isolated to the flow that caused it; it affected every other flow running at the same time. + +`ForEachItem` addressed this by dispatching each batch to a separate subflow execution, which kept the task run count manageable. But it required splitting your logic into a separate flow, passing data through inputs and outputs across a flow boundary, and managing the lifecycle of subflow executions. For simple per-item processing, this overhead was hard to justify. + +`Loop` fixes the stability problem and simplifies the model. Each iteration runs as an **isolated sub-execution**, so no single flow can generate unbounded task runs in one execution. The child tasks live inline — no separate flow required — and each iteration's failure is contained. A badly-sized loop degrades gracefully rather than destabilizing the instance. + +The expression syntax also improves. In `ForEach`, accessing the current value from inside a nested flowable required `parent.taskrun.value` or `parents[0].taskrun.value`. In `Loop`, `item` is bound to the sub-execution itself, so all tasks — including those inside nested `If`, `Parallel`, or other flowables — access it as `item.value` with no parent traversal required. + +## Expression quick reference + +The table below maps every `ForEach` expression to its `Loop` equivalent. The sections that follow show complete before-and-after examples for each pattern. + +| ForEach expression | Loop equivalent | Notes | +|---|---|---| +| `taskrun.value` | `item.value` | | +| `taskrun.iteration` | `item.index` | Zero-based in both | +| `parent.taskrun.value` | `item.value` | No prefix needed inside nested flowables | +| `parents[0].taskrun.value` | `item.parent.value` | Inside the inner of two nested loops | +| `parents[1].taskrun.value` | `item.parents[0].value` | One level further up | +| `outputs.task_id[taskrun.value].value` | `outputs.task_id.value` | Inside the iteration; task outputs are scoped to the current sub-execution | +| `outputs.foreach_id[value].field` (after the loop) | `outputs.loop_id.outputs['value'].output_id` (after the loop) | Outside the loop; collected outputs are keyed by iteration value string | + +## Migrating `ForEach` + +For most flows, the `ForEach` migration has three parts: replace the task type, update expressions inside child tasks, and update any post-loop output access. The sections below cover each pattern with before-and-after examples. + +### Basic iteration + +Replace `io.kestra.plugin.core.flow.ForEach` with `io.kestra.plugin.core.flow.Loop`. Inside child tasks, replace `{{ taskrun.value }}` with `{{ item.value }}` and `{{ taskrun.iteration }}` with `{{ item.index }}`. + +**Before** + +```yaml +tasks: + - id: for_each + type: io.kestra.plugin.core.flow.ForEach + values: ["value 1", "value 2", "value 3"] + tasks: + - id: log + type: io.kestra.plugin.core.log.Log + message: "value={{ taskrun.value }}" +``` + +**After** + +```yaml +tasks: + - id: loop + type: io.kestra.plugin.core.flow.Loop + values: ["value 1", "value 2", "value 3"] + tasks: + - id: log + type: io.kestra.plugin.core.log.Log + message: "index={{ item.index }} value={{ item.value }}" +``` + +### Nested flowables + +In `ForEach`, tasks inside a nested `If` or `Parallel` had to traverse up to the ForEach task run with `parent.taskrun.value` or `parents[0].taskrun.value`. In `Loop`, `item` is bound to the sub-execution and is accessible directly from any depth — no traversal needed. + +**Before** + +```yaml +tasks: + - id: for_each + type: io.kestra.plugin.core.flow.ForEach + values: ["value 1", "value 2", "value 3"] + tasks: + - id: check + type: io.kestra.plugin.core.flow.If + condition: '{{ taskrun.value == "value 2" }}' + then: + - id: matched + type: io.kestra.plugin.core.log.Log + message: "Matched at {{ parents[0].taskrun.value }}" + else: + - id: skipped + type: io.kestra.plugin.core.log.Log + message: "Skipped: {{ parents[0].taskrun.value }}" +``` + +**After** + +```yaml +tasks: + - id: loop + type: io.kestra.plugin.core.flow.Loop + values: ["value 1", "value 2", "value 3"] + tasks: + - id: check + type: io.kestra.plugin.core.flow.If + condition: '{{ item.value == "value 2" }}' + then: + - id: matched + type: io.kestra.plugin.core.log.Log + message: "Matched at index={{ item.index }}: {{ item.value }}" + else: + - id: skipped + type: io.kestra.plugin.core.log.Log + message: "Skipped: {{ item.value }}" +``` + +### Concurrent execution + +The `concurrencyLimit` property carries over unchanged. Update only the task type and the expressions inside child tasks. + +**Before** + +```yaml +tasks: + - id: for_each + type: io.kestra.plugin.core.flow.ForEach + values: [1, 2, 3, 4, 5] + concurrencyLimit: 3 + tasks: + - id: log + type: io.kestra.plugin.core.log.Log + message: "Processing {{ taskrun.value }}" +``` + +**After** + +```yaml +tasks: + - id: loop + type: io.kestra.plugin.core.flow.Loop + values: [1, 2, 3, 4, 5] + concurrencyLimit: 3 + tasks: + - id: log + type: io.kestra.plugin.core.log.Log + message: "Processing {{ item.value }} (index={{ item.index }})" +``` + +### Nested loops + +Nested loops work the same structurally. The change is how you reference outer loop values: `item.parent.value` replaces `parents[0].taskrun.value`, and for deeper hierarchies `item.parents[n]` replaces `parents[n+1].taskrun.value`. + +**Before** + +```yaml +tasks: + - id: outer + type: io.kestra.plugin.core.flow.ForEach + values: [1, 2, 3] + tasks: + - id: inner + type: io.kestra.plugin.core.flow.ForEach + values: ["a", "b"] + tasks: + - id: log + type: io.kestra.plugin.core.log.Log + message: "outer={{ parents[0].taskrun.value }} inner={{ taskrun.value }}" +``` + +**After** + +```yaml +tasks: + - id: outer + type: io.kestra.plugin.core.flow.Loop + values: [1, 2, 3] + tasks: + - id: inner + type: io.kestra.plugin.core.flow.Loop + values: ["a", "b"] + tasks: + - id: log + type: io.kestra.plugin.core.log.Log + message: "outer={{ item.parent.value }} inner={{ item.value }}" +``` + +### Failure handling + +`ForEach` required wrapping tasks in `AllowFailure` to continue past failures. `Loop` replaces this with a first-class `transmitFailed` property. Set `transmitFailed: false` on the Loop task and remove the `AllowFailure` wrapper entirely. + +**Before** + +```yaml +tasks: + - id: for_each + type: io.kestra.plugin.core.flow.ForEach + values: ["ok", "fail", "ok"] + tasks: + - id: guard + type: io.kestra.plugin.core.flow.AllowFailure + tasks: + - id: maybe_fail + type: io.kestra.plugin.core.flow.If + condition: '{{ taskrun.value == "fail" }}' + then: + - id: do_fail + type: io.kestra.plugin.core.execution.Fail + else: + - id: success + type: io.kestra.plugin.core.log.Log + message: "OK: {{ taskrun.value }}" +``` + +**After** + +```yaml +tasks: + - id: loop + type: io.kestra.plugin.core.flow.Loop + values: ["ok", "fail", "ok"] + transmitFailed: false + tasks: + - id: maybe_fail + type: io.kestra.plugin.core.flow.If + condition: '{{ item.value == "fail" }}' + then: + - id: do_fail + type: io.kestra.plugin.core.execution.Fail + else: + - id: success + type: io.kestra.plugin.core.log.Log + message: "OK: {{ item.value }}" +``` + +### Iterating over a map + +`Loop` adds native support for map values — `ForEach` had no equivalent. If your flows previously worked around this limitation by serializing maps or splitting keys and values, you can simplify them. When `values` is a map, `item.key` holds the key and `item.value` holds the associated value. + +```yaml +tasks: + - id: loop + type: io.kestra.plugin.core.flow.Loop + values: + dev: http://dev.example.com + staging: http://staging.example.com + prod: http://prod.example.com + tasks: + - id: log + type: io.kestra.plugin.core.log.Log + message: "env={{ item.key }} url={{ item.value }}" +``` + +### Outputs + +In `ForEach`, task outputs from all iterations were automatically merged into a single map in the parent execution, keyed by `taskrun.value`. Any task after the loop could access `outputs.task_id[value].field` without any extra configuration. + +In `Loop`, each iteration runs in its own sub-execution. Task outputs inside an iteration are **not** visible outside the loop by default. You must explicitly declare which values to expose using the `outputs` property on the Loop task. After the loop completes, the collected outputs are available as a map keyed by iteration value: `outputs..outputs[''].`. + +**Before** + +```yaml +tasks: + - id: for_each + type: io.kestra.plugin.core.flow.ForEach + values: ["a", "b", "c"] + tasks: + - id: process + type: io.kestra.plugin.core.debug.Return + format: "processed {{ taskrun.value }}" + + - id: summary + type: io.kestra.plugin.core.log.Log + message: "Results: {{ outputs.process | jq('[.[].value]') }}" +``` + +**After** + +```yaml +tasks: + - id: loop + type: io.kestra.plugin.core.flow.Loop + values: ["a", "b", "c"] + outputs: + - id: result + type: STRING + value: "{{ outputs.process.value }}" + tasks: + - id: process + type: io.kestra.plugin.core.debug.Return + format: "processed {{ item.value }}" + + - id: summary + type: io.kestra.plugin.core.log.Log + message: | + Loop ran {{ outputs.loop.iterationCount }} iterations. + All results: {{ outputs.loop.outputs | toJson }} + Result for 'a': {{ outputs.loop.outputs['a'].result }} +``` + +## Migrating `ForEachItem` + +`ForEachItem` dispatched each batch to a separate subflow execution. With `Loop`, you process each item inline — no separate flow required. When `values` is an internal storage URI, Kestra iterates over each line of the file and runs the child tasks for that line, with `item.value` holding the content of each line. + +For flows that used `ForEachItem` with `batch.rows: 1`, the migration is a direct substitution: replace the `ForEachItem` block and its subflow with a single `Loop` task with inline tasks. + +**Before** + +```yaml +tasks: + - id: each_item + type: io.kestra.plugin.core.flow.ForEachItem + items: "{{ inputs.file }}" + batch: + rows: 1 + wait: true + namespace: company.team + flowId: process_item + inputs: + item: "{{ taskrun.items }}" +``` + +**After** + +```yaml +tasks: + - id: loop + type: io.kestra.plugin.core.flow.Loop + values: "{{ inputs.file }}" + tasks: + - id: process + type: io.kestra.plugin.core.log.Log + message: "Processing: {{ item.value }}" +``` + +For flows that relied on batch sizes larger than 1, you will need to restructure: either reduce to single-item processing, or keep a `Subflow` task inside a `Loop` and handle batching within that subflow. + +## Migration steps + +1. Search all flows for `io.kestra.plugin.core.flow.ForEach` and replace the task type with `io.kestra.plugin.core.flow.Loop`. +2. Replace every `{{ taskrun.value }}` inside the loop with `{{ item.value }}`. +3. Replace every `{{ taskrun.iteration }}` inside the loop with `{{ item.index }}`. +4. Remove `parent.taskrun.value` and `parents[0].taskrun.value` references inside nested flowables — `{{ item.value }}` works directly at any nesting depth. +5. Update post-loop output access: declare `outputs` on the Loop task, then access collected results via `outputs..outputs[''].`. The map is keyed by iteration value string. +6. Search all flows for `io.kestra.plugin.core.flow.ForEachItem` and migrate to `Loop` with a URI value as described above. +7. Validate each updated flow by saving it in the Kestra UI or via the API and confirming no parse errors. From fa75dffbd52ea030c07f1c49f265f406d7180fd3 Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Mon, 27 Apr 2026 13:44:47 +0200 Subject: [PATCH 30/52] docs(s3-files): add configuration Part of https://github.com/kestra-io/storage-s3/issues/197 --- .../02.runtime-and-storage/index.md | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/contents/docs/configuration/02.runtime-and-storage/index.md b/src/contents/docs/configuration/02.runtime-and-storage/index.md index cf9f6be5d29..7290c762ec3 100644 --- a/src/contents/docs/configuration/02.runtime-and-storage/index.md +++ b/src/contents/docs/configuration/02.runtime-and-storage/index.md @@ -225,6 +225,7 @@ Common options include: - `local` for local testing - `s3` +- `s3files` - `gcs` - `azure` - `minio` @@ -314,6 +315,44 @@ kestra: sts-endpoint-override: "" ``` +#### S3 Files compatibility mode + +If your bucket has [AWS S3 Files](https://aws.amazon.com/blogs/aws/launching-s3-files-making-s3-buckets-accessible-as-file-systems/) enabled, set `s3-files-compatible: true`. S3 Files requires bucket versioning, so Kestra enables versioning on the bucket during startup. Kestra will also hard-delete all object versions and delete markers instead of writing a single delete marker. + +```yaml +kestra: + storage: + type: s3 + s3: + region: "" + bucket: "" + s3-files-compatible: true +``` + +:::alert{type="warning"} +Do not enable this flag on a plain S3 bucket that is not using S3 Files. Once versioning is enabled on a bucket it cannot be fully disabled, only suspended. +::: + +### S3 Files + +Use `s3files` when Kestra runs on a host where an [S3 Files](https://aws.amazon.com/blogs/aws/launching-s3-files-making-s3-buckets-accessible-as-file-systems/) NFS filesystem is already mounted locally. This backend reads and writes directly through the local filesystem — no S3 SDK or AWS credentials are required. + +Mount the NFS filesystem on every host that runs a Kestra component before configuring this backend. All components must share the same mount path. + +```yaml +kestra: + storage: + type: s3files + s3files: + mount-path: "/mnt/s3files" +``` + +`mount-path` must point to a directory that exists and is readable and writable by the Kestra process. Kestra will not create the directory on startup. + +Object metadata is stored in `.meta` sidecar files alongside each object on the filesystem. Custom S3 object metadata is not exposed through this backend. + +If you prefer to keep the S3 SDK path (for example, because not every host has the NFS mount), use the standard `s3` backend with `s3-files-compatible: true` instead. + ### MinIO MinIO is a good self-hosted choice when you want object storage behavior without depending on a cloud provider: From 7d127915b5cf8a45244106eed2b9c37eeb89bc39 Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Mon, 4 May 2026 16:45:26 +0200 Subject: [PATCH 31/52] docs(plugin-defaults): forced on flow level Part of https://github.com/kestra-io/kestra-ee/issues/7109 --- .../09.plugin-defaults/index.md | 20 ++----- .../plugin-defaults-forced-removed/index.md | 58 +++++++++++++++++++ 2 files changed, 64 insertions(+), 14 deletions(-) create mode 100644 src/contents/docs/11.migration-guide/v2.0.0/plugin-defaults-forced-removed/index.md diff --git a/src/contents/docs/05.workflow-components/09.plugin-defaults/index.md b/src/contents/docs/05.workflow-components/09.plugin-defaults/index.md index c3b99c9c81f..b2f6b34b1d6 100644 --- a/src/contents/docs/05.workflow-components/09.plugin-defaults/index.md +++ b/src/contents/docs/05.workflow-components/09.plugin-defaults/index.md @@ -9,17 +9,13 @@ docId: plugin-defaults Plugin defaults are default values applied to every task of a given type within one or more flows. -## Plugin Defaults – set task-level defaults - They work like default function arguments, helping you avoid repetition when tasks or plugins frequently use the same values.
---- - -## Plugin Defaults on a flow-level +## Plugin defaults at the flow level You can define plugin defaults in the `pluginDefaults` section to avoid repeating properties across multiple tasks of the same type. For example: @@ -70,7 +66,7 @@ pluginDefaults: containerImage: python:slim ``` -In this example, Docker and Python configurations are defined once in `pluginDefaults`, instead of being repeated in every task. This approach helps to streamline the configuration process and reduce the chances of errors caused by inconsistent settings across different tasks. +In this example, Docker and Python configurations are defined once in `pluginDefaults` rather than repeated in every task. :::alert{type="info"} If you move required attributes into `pluginDefaults`, the UI code editor may show warnings about missing arguments, because defaults are only resolved at runtime. As long as `pluginDefaults` contains the relevant arguments, you can save the flow and ignore the warning displayed in the editor. @@ -79,13 +75,9 @@ If you move required attributes into `pluginDefaults`, the UI code editor may sh ::: -### `forced` attribute in `pluginDefaults` - -Setting `forced: true` in `pluginDefaults` ensures that default values override any properties defined directly in the task. By default, the value of the `forced` attribute is `false`. - ## Plugin defaults in a global configuration -Plugin defaults can also be defined globally in your Kestra configuration, applying the same values across all flows. This is useful when you want to apply the same defaults across multiple flows. Let's say that you want to centrally manage the default values for the `io.kestra.plugin.aws` plugin to reuse the same credentials and region across all your flows. You can add the following to your Kestra configuration: +Plugin defaults can also be defined globally in your Kestra configuration, applying the same values across all flows. To centrally manage credentials for the `io.kestra.plugin.aws` plugin, add the following to your Kestra configuration: ```yaml kestra: @@ -104,7 +96,7 @@ Global plugin defaults must be configured under `kestra.plugins.defaults`. The legacy `kestra.tasks.defaults` property is still supported for backward compatibility, but it is deprecated. Use `kestra.plugins.defaults` for all new configurations. ::: -If you want to set defaults only for a specific task, you can do that too: +To set defaults for a specific task type only: ```yaml kestra: @@ -125,9 +117,9 @@ Kestra applies plugin defaults in this order: 2. Flow-level `pluginDefaults` 3. Properties defined directly on the task -That means flow-level defaults override global defaults, and task properties override non-forced defaults. +That means flow-level defaults override global defaults, and task properties override flow-level defaults. -If `forced: true` is set on a plugin default, that default overrides properties defined directly on the task. This is especially useful for governance and isolation use cases such as enforcing a task runner. +Global configuration and namespace-level plugin defaults support a `forced` property. Setting `forced: true` on a global or namespace-level default makes it override any value set directly on the task. This is intended for governance use cases — for example, enforcing a specific task runner across all flows in a namespace. ## Plugin Defaults Enterprise Edition diff --git a/src/contents/docs/11.migration-guide/v2.0.0/plugin-defaults-forced-removed/index.md b/src/contents/docs/11.migration-guide/v2.0.0/plugin-defaults-forced-removed/index.md new file mode 100644 index 00000000000..9eae12835bf --- /dev/null +++ b/src/contents/docs/11.migration-guide/v2.0.0/plugin-defaults-forced-removed/index.md @@ -0,0 +1,58 @@ +--- +title: pluginDefaults.forced Removed from Flows +sidebarTitle: pluginDefaults.forced Removed from Flows +icon: /src/contents/docs/icons/migration-guide.svg +release: 2.0.0 +editions: ["OSS", "EE"] +description: The forced property is removed from flow-level pluginDefaults in Kestra 2.0. Use namespace-level Plugin Defaults or global configuration to enforce defaults that tasks cannot override. +--- + +The `forced` property is removed from flow-level `pluginDefaults` in Kestra 2.0. + +:::alert{type="warning"} +Flows that include `forced: true` inside a `pluginDefaults` block will fail to parse after upgrading to 2.0.0. Remove this property before upgrading. +::: + +## Why the change + +`forced: true` in a flow's `pluginDefaults` let a flow author override any value a task explicitly set. This created a security problem: a regular user editing a flow could use `forced: true` to override plugin defaults that a platform administrator had configured at the namespace or tenant level. Platform administrators are now solely responsible for enforcing defaults, and must do so at the namespace or global configuration level. + +Flow-level `pluginDefaults` continue to work for setting convenient defaults — they just can no longer override what a task explicitly declares. + +## Migration steps + +1. Search all flows for `pluginDefaults` blocks that include `forced: true`. +2. Remove the `forced: true` line from each flow. +3. If you need to prevent tasks from overriding a default, move that default to the namespace **Plugin Defaults** tab (Enterprise Edition) or to the `kestra.plugins.defaults` section of your global Kestra configuration. + +**Before** + +```yaml +pluginDefaults: + - type: io.kestra.plugin.scripts.runner.docker.Docker + forced: true + values: + pullPolicy: NEVER +``` + +**After** + +```yaml +pluginDefaults: + - type: io.kestra.plugin.scripts.runner.docker.Docker + values: + pullPolicy: NEVER +``` + +To enforce the value so tasks cannot override it, configure `forced: true` at the global or namespace level instead: + +```yaml +# kestra.yml — global configuration +kestra: + plugins: + defaults: + - type: io.kestra.plugin.scripts.runner.docker.Docker + forced: true + values: + pullPolicy: NEVER +``` From 42cbc261ed204cf943a119d98dcb9ba0b8d9eb87 Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Tue, 5 May 2026 09:26:05 +0200 Subject: [PATCH 32/52] docs(k8s-runner): add connection-concurrency section Part of https://github.com/kestra-io/plugin-ee-kubernetes/issues/119 --- .../03.kubernetes-task-runner/index.md | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/contents/docs/task-runners/04.types/03.kubernetes-task-runner/index.md b/src/contents/docs/task-runners/04.types/03.kubernetes-task-runner/index.md index fbef02271f7..f7e39e5056e 100644 --- a/src/contents/docs/task-runners/04.types/03.kubernetes-task-runner/index.md +++ b/src/contents/docs/task-runners/04.types/03.kubernetes-task-runner/index.md @@ -291,6 +291,29 @@ taskRunner: caCertData: "{{ secret('K8S_CA_CERT_DATA') }}" ``` +## Connection and concurrency settings + +At high concurrency, each task opens multiple WebSocket connections against the API server — one for the pod watch, one for the log stream, and one or two for file upload and sidecar signaling. On clusters that enforce API rate limits (such as GKE), this can cause transient failures and make timeout issues worse by slowing API server responses. + +Three properties on the `config:` block let you cap concurrent connections and tune reconnect backoff: + +| Property | Default | Description | +|---|---|---| +| `maxConcurrentRequests` | `64` | Maximum total concurrent HTTP requests per client. | +| `maxConcurrentRequestsPerHost` | `5` | Maximum concurrent HTTP requests to the API server host. | +| `watchReconnectInterval` | `PT1S` | Backoff between watch reconnects. Increase to prevent reconnect storms under API pressure. | + +```yaml +taskRunner: + type: io.kestra.plugin.ee.kubernetes.runner.Kubernetes + config: + masterUrl: https://docker-for-desktop:6443 + caCertData: "{{ secret('K8S_CA_CERT_DATA') }}" + maxConcurrentRequests: 32 + maxConcurrentRequestsPerHost: 3 + watchReconnectInterval: PT5S +``` + ## Pod and container customization The Kubernetes task runner exposes several properties for customizing the pod spec beyond standard options like `resources` and `namespace`. These are advanced properties intended for cases such as security hardening, shared volumes, custom sidecars, or node scheduling constraints. From 3ef5015c8caf09620cd01fd2572d653069737640 Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Tue, 5 May 2026 09:29:55 +0200 Subject: [PATCH 33/52] docs(logshipper): add Dash0 Part of https://github.com/kestra-io/kestra-ee/issues/7568 --- .../02.governance/logshipper/index.md | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/contents/docs/07.enterprise/02.governance/logshipper/index.md b/src/contents/docs/07.enterprise/02.governance/logshipper/index.md index 136bf2fe8f2..44cdf20d730 100644 --- a/src/contents/docs/07.enterprise/02.governance/logshipper/index.md +++ b/src/contents/docs/07.enterprise/02.governance/logshipper/index.md @@ -18,7 +18,7 @@ Manage and distribute logs across your entire infrastructure. Log Shipper can distribute Kestra logs from across your instance to an external logging platform. Log synchronization fetches logs and batches them into optimized chunks automatically. The batch process is done intelligently through defined synchronization points. Once batched, the Log Shipper delivers consistent and reliable data to your monitoring platform. -Log Shipper is built on top of [Kestra plugins](/plugins), ensuring it can integrate with popular logging platforms and expand as more plugins are developed. Supported observability platforms include ElasticSearch, Datadog, New Relic, Azure Monitor, Google Operational Suite, AWS Cloudwatch, Splunk, OpenSearch, and OpenTelemetry. +Log Shipper is built on top of [Kestra plugins](/plugins), ensuring it can integrate with popular logging platforms and expand as more plugins are developed. Supported observability platforms include ElasticSearch, Datadog, New Relic, Azure Monitor, Google Operational Suite, AWS Cloudwatch, Splunk, OpenSearch, OpenTelemetry, and Dash0. ## Log shipper properties @@ -490,6 +490,32 @@ tasks: chunk: 1000 ``` +### Dash0 + +This example exports logs to [Dash0](https://www.dash0.com/). The following example flow triggers a daily batch and exports to Dash0 via OTLP/HTTP. Set the `endpoint` to the ingestion URL for your Dash0 region and the `dataset` to route logs to a specific Dash0 dataset (defaults to `default` when omitted). + +```yaml +id: log_shipper +namespace: company.team + +triggers: + - id: daily + type: io.kestra.plugin.core.trigger.Schedule + cron: "@daily" + +tasks: + - id: log_export + type: io.kestra.plugin.ee.core.log.LogShipper + logLevelFilter: INFO + lookbackPeriod: P1D + logExporters: + - id: dash0LogExporter + type: io.kestra.plugin.ee.dash0.LogExporter + endpoint: https://ingress.eu-west-1.aws.dash0.com/v1/logs + authToken: "{{ secret('DASH0_AUTH_TOKEN') }}" + dataset: my-dataset +``` + ## Audit log shipper To send [Audit Logs](../06.audit-logs/index.md) to an external system, there is the Audit Log Shipper task type. The Audit Log Shipper task extracts logs from the Kestra backend and loads them to desired destinations including Datadog, Elasticsearch, New Relic, OpenTelemetry, AWS CloudWatch, Google Operational Suite, and Azure Monitor. From 9ba65b768eb2eb84650413ddd73acca52aa4d8e0 Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Tue, 5 May 2026 09:36:36 +0200 Subject: [PATCH 34/52] docs(aws): ensure mention of image Part of https://github.com/kestra-io/plugin-ee-aws/issues/113 --- .../task-runners/04.types/04.aws-batch-task-runner/index.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/contents/docs/task-runners/04.types/04.aws-batch-task-runner/index.md b/src/contents/docs/task-runners/04.types/04.aws-batch-task-runner/index.md index 433c56937ad..8c854808ae6 100644 --- a/src/contents/docs/task-runners/04.types/04.aws-batch-task-runner/index.md +++ b/src/contents/docs/task-runners/04.types/04.aws-batch-task-runner/index.md @@ -30,6 +30,8 @@ To support `inputFiles`, `namespaceFiles`, and `outputFiles`, the task runner cr 2. The _main_ container that fetches input files into the `{{ workingDir }}` directory and runs the task. 3. An _after_-container that fetches output files using `outputFiles` to make them available from the Kestra UI for download and preview. +The before- and after-containers use the `amazon/aws-cli` image. If your environment restricts which images can be pulled (ECR pull-through cache, VPC egress policy, or image allowlist), ensure this image is accessible. + **EKS:** Uses [EKS job definitions](https://docs.aws.amazon.com/batch/latest/userguide/jobs-eks.html) with a Kubernetes pod. Sidecar containers run as pod containers using the same S3-based file transfer pattern. The main container command is wrapped in `/bin/sh -c`, so the container image must include `/bin/sh`. Since the working directory of the container isn’t known in advance, you must define the working and output directories explicitly. For example, use `cat {{ workingDir }}/myFile.txt` instead of `cat myFile.txt`. From d31280e97206c29f3a709c3c1d8fe1a35a7e4b8a Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Tue, 5 May 2026 09:45:01 +0200 Subject: [PATCH 35/52] docs(fs): add recursive migration guide Part of https://github.com/kestra-io/plugin-fs/issues/302 --- .../local-delete-recursive-default/index.md | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/contents/docs/11.migration-guide/v2.0.0/local-delete-recursive-default/index.md diff --git a/src/contents/docs/11.migration-guide/v2.0.0/local-delete-recursive-default/index.md b/src/contents/docs/11.migration-guide/v2.0.0/local-delete-recursive-default/index.md new file mode 100644 index 00000000000..3369c8d0daa --- /dev/null +++ b/src/contents/docs/11.migration-guide/v2.0.0/local-delete-recursive-default/index.md @@ -0,0 +1,41 @@ +--- +title: local.Delete recursive Default Changed to false +sidebarTitle: local.Delete recursive Default +icon: /src/contents/docs/icons/migration-guide.svg +release: 2.0.0 +editions: ["OSS", "EE"] +description: The recursive property of io.kestra.plugin.fs.local.Delete now defaults to false. Flows that delete a directory without setting recursive explicitly will stop removing subdirectory contents after upgrading. +--- + +The `recursive` property of `io.kestra.plugin.fs.local.Delete` now defaults to `false` instead of `true`. + +:::alert{type="warning"} +Flows that call `io.kestra.plugin.fs.local.Delete` on a directory without setting `recursive` explicitly will silently stop deleting subdirectory contents after upgrading to 2.0.0. No error is raised — the task will succeed but leave nested files in place. +::: + +## Why the change + +The previous default of `true` made directory deletions recursive without any explicit opt-in. A misconfigured `from` path could wipe an entire directory tree. The new default of `false` matches the behavior of every other `Delete` task in `plugin-fs` (SFTP, FTP, NFS, SMB) and requires users to opt in to recursive deletion deliberately. + +## Migration steps + +1. Search all flows for tasks of type `io.kestra.plugin.fs.local.Delete`. +2. For each task where `from` points to a directory and `recursive` is not set, add `recursive: true` to preserve the previous behavior. +3. For tasks where `from` points to a single file, no change is needed — `recursive` has no effect on file targets. + +**Before** (recursive deletion happened implicitly) + +```yaml +- id: cleanup + type: io.kestra.plugin.fs.local.Delete + from: /data/uploads/processed/ +``` + +**After** (opt in explicitly to keep the same behavior) + +```yaml +- id: cleanup + type: io.kestra.plugin.fs.local.Delete + from: /data/uploads/processed/ + recursive: true +``` From 68472094c955d1b177efcb6938dd09ffb49fb397 Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Tue, 5 May 2026 09:47:05 +0200 Subject: [PATCH 36/52] docs(style): small style edits to new content --- .../docs/07.enterprise/02.governance/logshipper/index.md | 2 +- .../v2.0.0/local-delete-recursive-default/index.md | 4 ++-- .../task-runners/04.types/03.kubernetes-task-runner/index.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/contents/docs/07.enterprise/02.governance/logshipper/index.md b/src/contents/docs/07.enterprise/02.governance/logshipper/index.md index 44cdf20d730..d6b7e62edbf 100644 --- a/src/contents/docs/07.enterprise/02.governance/logshipper/index.md +++ b/src/contents/docs/07.enterprise/02.governance/logshipper/index.md @@ -492,7 +492,7 @@ tasks: ### Dash0 -This example exports logs to [Dash0](https://www.dash0.com/). The following example flow triggers a daily batch and exports to Dash0 via OTLP/HTTP. Set the `endpoint` to the ingestion URL for your Dash0 region and the `dataset` to route logs to a specific Dash0 dataset (defaults to `default` when omitted). +This example exports logs to [Dash0](https://www.dash0.com/) via OTLP/HTTP. Set `endpoint` to the ingestion URL for your Dash0 region. Set `dataset` to route logs to a named dataset, or omit it to use the Dash0 `default` dataset. ```yaml id: log_shipper diff --git a/src/contents/docs/11.migration-guide/v2.0.0/local-delete-recursive-default/index.md b/src/contents/docs/11.migration-guide/v2.0.0/local-delete-recursive-default/index.md index 3369c8d0daa..8128b2e88e5 100644 --- a/src/contents/docs/11.migration-guide/v2.0.0/local-delete-recursive-default/index.md +++ b/src/contents/docs/11.migration-guide/v2.0.0/local-delete-recursive-default/index.md @@ -15,7 +15,7 @@ Flows that call `io.kestra.plugin.fs.local.Delete` on a directory without settin ## Why the change -The previous default of `true` made directory deletions recursive without any explicit opt-in. A misconfigured `from` path could wipe an entire directory tree. The new default of `false` matches the behavior of every other `Delete` task in `plugin-fs` (SFTP, FTP, NFS, SMB) and requires users to opt in to recursive deletion deliberately. +The previous default of `true` made directory deletions recursive without any explicit opt-in. A misconfigured `from` path could wipe an entire directory tree. The new default of `false` matches the behavior of every other `Delete` task in `plugin-fs` (SFTP, FTP, NFS, SMB) and requires an explicit opt-in for recursive deletion. ## Migration steps @@ -31,7 +31,7 @@ The previous default of `true` made directory deletions recursive without any ex from: /data/uploads/processed/ ``` -**After** (opt in explicitly to keep the same behavior) +**After** (opt in to keep the same behavior) ```yaml - id: cleanup diff --git a/src/contents/docs/task-runners/04.types/03.kubernetes-task-runner/index.md b/src/contents/docs/task-runners/04.types/03.kubernetes-task-runner/index.md index f7e39e5056e..743075cdfd6 100644 --- a/src/contents/docs/task-runners/04.types/03.kubernetes-task-runner/index.md +++ b/src/contents/docs/task-runners/04.types/03.kubernetes-task-runner/index.md @@ -293,7 +293,7 @@ taskRunner: ## Connection and concurrency settings -At high concurrency, each task opens multiple WebSocket connections against the API server — one for the pod watch, one for the log stream, and one or two for file upload and sidecar signaling. On clusters that enforce API rate limits (such as GKE), this can cause transient failures and make timeout issues worse by slowing API server responses. +At high concurrency, each task opens multiple WebSocket connections against the API server — one for the pod watch, one for the log stream, and one or two for file upload and sidecar signaling. On clusters that enforce API rate limits (such as GKE), this can cause transient failures and slow API server responses, compounding timeout issues. Three properties on the `config:` block let you cap concurrent connections and tune reconnect backoff: From 9105204ade7cee4b6f195752addf3300b78c3ab1 Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Tue, 5 May 2026 09:55:34 +0200 Subject: [PATCH 37/52] docs(outputfiles): script output files Part of https://github.com/kestra-io/kestra/issues/13765 --- .../16.scripts/07.input-output-files/index.md | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/contents/docs/16.scripts/07.input-output-files/index.md b/src/contents/docs/16.scripts/07.input-output-files/index.md index 17afb29d6a3..7279425058c 100644 --- a/src/contents/docs/16.scripts/07.input-output-files/index.md +++ b/src/contents/docs/16.scripts/07.input-output-files/index.md @@ -148,3 +148,31 @@ tasks: Note how the `outputFiles` property is used to specify the list of files to be persisted in Kestra's internal storage. The `outputFiles` property supports [glob patterns](https://en.wikipedia.org/wiki/Glob_(programming)). The subsequent task can access the output file by leveraging the syntax `{{outputs.yourTaskId.outputFiles['yourFileName.fileExtension']}}`. + +### Referencing output file paths inside the script + +For local runners (Process and Docker), writing files by plain name works because Kestra sets the process working directory automatically. For remote task runners (Kubernetes, AWS Batch, Azure Batch, etc.), the working directory is an execution-specific absolute path. Rather than constructing it manually with `{{ workingDir }}/filename`, you can use `{{ outputFiles["filename"] }}` to get the resolved absolute path by name: + +```yaml +id: output_file_remote +namespace: company.team + +tasks: + - id: shell + type: io.kestra.plugin.scripts.shell.Commands + taskRunner: + type: io.kestra.plugin.ee.kubernetes.runner.Kubernetes + config: + masterUrl: https://my-cluster:6443 + caCertData: "{{ secret('K8S_CA_CERT_DATA') }}" + outputFiles: + - out.txt + commands: + - echo "Hello from Kubernetes" > {{ outputFiles["out.txt"] }} +``` + +The `{{ outputFiles["filename"] }}` expression resolves to the absolute path of the named file in the task's working directory — the same value as `{{ workingDir }}/filename`. The same form is used in JDBC tasks (e.g., `COPY ... TO '{{ outputFiles["out.csv"] }}'`), so the pattern is consistent across task types. + +:::alert{type="info"} +Only named files are available as Pebble expressions. Glob patterns such as `*.csv` are collected post-run but cannot be referenced as `{{ outputFiles["*.csv"] }}` inside the script. Declare named files for any output you need to reference by path, and use globs only for bulk collection. +::: From eba7436f7170818a7efe41f184aa072b4b86bb6d Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Fri, 8 May 2026 12:16:05 +0200 Subject: [PATCH 38/52] docs(runIf): revert change to when Part of https://github.com/kestra-io/kestra-ee/issues/7695 --- .../05.workflow-components/01.tasks/index.mdx | 2 +- .../06.outputs/index.md | 2 +- .../07.triggers/01.schedule-trigger/index.md | 2 +- .../20.afterexecution/index.md | 6 +-- .../docs/09.ui/02.executions/index.md | 2 +- .../v2.0.0/run-if-renamed-when/index.md | 43 ------------------- .../docs/14.best-practices/7.outputs/index.md | 2 +- .../ansible-config-drift/index.md | 2 +- .../subflow-executions/index.md | 2 +- .../docs/use-cases/03.microservices/index.md | 8 ++-- 10 files changed, 14 insertions(+), 57 deletions(-) delete mode 100644 src/contents/docs/11.migration-guide/v2.0.0/run-if-renamed-when/index.md diff --git a/src/contents/docs/05.workflow-components/01.tasks/index.mdx b/src/contents/docs/05.workflow-components/01.tasks/index.mdx index 30c09d76620..39896e66006 100644 --- a/src/contents/docs/05.workflow-components/01.tasks/index.mdx +++ b/src/contents/docs/05.workflow-components/01.tasks/index.mdx @@ -57,7 +57,7 @@ All tasks share the following core properties: | `description` | Your custom [documentation](../../../05.workflow-components/15.descriptions/index.md) of what the task does | | `retry` | How often should the task be retried in case of a failure, and the [type of retry strategy](../../../05.workflow-components/12.retries/index.md) | | `timeout` | The [maximum time allowed](../../../05.workflow-components/13.timeout/index.md) for the task to complete expressed in [ISO 8601 Durations](https://en.wikipedia.org/wiki/ISO_8601#Durations) | -| `when` | Skip a task if the provided condition evaluates to false | +| `runIf` | Skip a task if the provided condition evaluates to false | | `disabled` | A boolean flag indicating whether the task is [disabled or not](../../../05.workflow-components/16.disabled/index.md); if set to `true`, the task will be skipped during the execution | | `workerGroup` | The [group of workers](../../07.enterprise/04.scalability/worker-group/index.md) (EE-only) that are eligible to execute the task; you can specify a `workerGroup.key` and a `workerGroup.fallback` (the default is `WAIT`) | | `allowFailure` | A boolean flag allowing to continue the execution even if this task fails | diff --git a/src/contents/docs/05.workflow-components/06.outputs/index.md b/src/contents/docs/05.workflow-components/06.outputs/index.md index 5c42d4146a2..30c2b32cb86 100644 --- a/src/contents/docs/05.workflow-components/06.outputs/index.md +++ b/src/contents/docs/05.workflow-components/06.outputs/index.md @@ -186,7 +186,7 @@ tasks: - id: main type: io.kestra.plugin.core.debug.Return format: Hello World! - when: "{{ inputs.run_task }}" + runIf: "{{ inputs.run_task }}" - id: fallback type: io.kestra.plugin.core.debug.Return diff --git a/src/contents/docs/05.workflow-components/07.triggers/01.schedule-trigger/index.md b/src/contents/docs/05.workflow-components/07.triggers/01.schedule-trigger/index.md index ff84674c079..b8d56800ff0 100644 --- a/src/contents/docs/05.workflow-components/07.triggers/01.schedule-trigger/index.md +++ b/src/contents/docs/05.workflow-components/07.triggers/01.schedule-trigger/index.md @@ -245,7 +245,7 @@ namespace: system tasks: - id: send_alert - when: "{{ trigger.data }}" + runIf: "{{ trigger.data }}" type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook url: https://kestra.io/api/mock messageText: The following Schedule triggers seem unhealthy {{ trigger.data }} diff --git a/src/contents/docs/05.workflow-components/20.afterexecution/index.md b/src/contents/docs/05.workflow-components/20.afterexecution/index.md index 875ae83f132..4ab48c54158 100644 --- a/src/contents/docs/05.workflow-components/20.afterexecution/index.md +++ b/src/contents/docs/05.workflow-components/20.afterexecution/index.md @@ -17,7 +17,7 @@ Run tasks after a flow execution completes. ## `afterExecution` property -`afterExecution` is a block of tasks that run after the flow ends. You can use it to run conditional tasks based on the final state, such as **SUCCESS** or **FAILED**. This is especially useful for custom notifications and alerts. For example, you can combine `afterExecution` with the `when` property to send different Slack messages depending on the execution state. +`afterExecution` is a block of tasks that run after the flow ends. You can use it to run conditional tasks based on the final state, such as **SUCCESS** or **FAILED**. This is especially useful for custom notifications and alerts. For example, you can combine `afterExecution` with the `runIf` property to send different Slack messages depending on the execution state. ```yaml id: alerts_demo @@ -29,13 +29,13 @@ tasks: afterExecution: - id: onSuccess - when: "{{execution.state == 'SUCCESS'}}" + runIf: "{{execution.state == 'SUCCESS'}}" type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook url: https://hooks.slack.com/services/xxxxx messageText: "{{flow.namespace}}.{{flow.id}} finished successfully!" - id: onFailure - when: "{{execution.state == 'FAILED'}}" + runIf: "{{execution.state == 'FAILED'}}" type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook url: https://hooks.slack.com/services/xxxxx messageText: "Oh no, {{flow.namespace}}.{{flow.id}} failed!!!" diff --git a/src/contents/docs/09.ui/02.executions/index.md b/src/contents/docs/09.ui/02.executions/index.md index ac870e9b1e4..7445179a313 100644 --- a/src/contents/docs/09.ui/02.executions/index.md +++ b/src/contents/docs/09.ui/02.executions/index.md @@ -38,7 +38,7 @@ inputs: tasks: - id: taskA - when: "{{ inputs.runTask }}" + runIf: "{{ inputs.runTask }}" type: io.kestra.plugin.core.debug.Return format: Hello World! diff --git a/src/contents/docs/11.migration-guide/v2.0.0/run-if-renamed-when/index.md b/src/contents/docs/11.migration-guide/v2.0.0/run-if-renamed-when/index.md deleted file mode 100644 index 02fbabee5bd..00000000000 --- a/src/contents/docs/11.migration-guide/v2.0.0/run-if-renamed-when/index.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: runIf Renamed to when on Tasks -sidebarTitle: runIf → when (Tasks) -icon: /src/contents/docs/icons/migration-guide.svg -release: 2.0.0 -editions: ["OSS", "EE"] -description: The task-level runIf property is renamed to when in Kestra 2.0, aligning it with the when property introduced on all triggers. ---- - -Kestra 2.0 introduces a `when` property on all triggers to replace the `conditions` list. To unify conditional execution under a single property name, the task-level `runIf` is renamed to `when` in the same release. - -No behavioral change. The Pebble expression is rendered at runtime and the task is set to `SKIPPED` if the result is falsy (`false`, `0`, `-0`, or an empty string) — identical to the existing `runIf` behavior. - -:::alert{type="warning"} -`runIf` is kept as a deprecated alias in 2.0 so existing flows continue to parse. The alias will be removed in a future release. Update your flows now to avoid a hard break later. -::: - -## Before - -```yaml -tasks: - - id: conditional_task - type: io.kestra.plugin.core.debug.Return - format: "Hello World!" - runIf: "{{ inputs.run_task }}" -``` - -## After - -```yaml -tasks: - - id: conditional_task - type: io.kestra.plugin.core.debug.Return - format: "Hello World!" - when: "{{ inputs.run_task }}" -``` - -## Migration steps - -1. **Search your flows** for `runIf:` and replace each occurrence with `when:`. The property value and any Pebble expressions stay the same. -2. **Validate** by saving the updated flows in the Kestra UI or via the API. - -For trigger condition changes (`conditions` → `when` on Schedule and Webhook triggers, `conditions`/`preconditions` → `dependsOn` on Flow triggers), see the [trigger conditions redesign guide](../trigger-conditions-redesign/index.md). diff --git a/src/contents/docs/14.best-practices/7.outputs/index.md b/src/contents/docs/14.best-practices/7.outputs/index.md index 32d49e13858..f3315635077 100644 --- a/src/contents/docs/14.best-practices/7.outputs/index.md +++ b/src/contents/docs/14.best-practices/7.outputs/index.md @@ -23,7 +23,7 @@ inputs: tasks: - id: taskA - when: "{{ inputs.runTask }}" + runIf: "{{ inputs.runTask }}" type: io.kestra.plugin.core.debug.Return format: Hello World! diff --git a/src/contents/docs/15.how-to-guides/ansible-config-drift/index.md b/src/contents/docs/15.how-to-guides/ansible-config-drift/index.md index 0c7bd22cc84..bc370a614f1 100644 --- a/src/contents/docs/15.how-to-guides/ansible-config-drift/index.md +++ b/src/contents/docs/15.how-to-guides/ansible-config-drift/index.md @@ -76,7 +76,7 @@ tasks: tasks: - id: check_drift type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook - when: "{{ taskrun.value | jq('.changed') | first == true }}" + runIf: "{{ taskrun.value | jq('.changed') | first == true }}" url: "{{ secret('SLACK_WEBHOOK') }}" payload: | { diff --git a/src/contents/docs/15.how-to-guides/subflow-executions/index.md b/src/contents/docs/15.how-to-guides/subflow-executions/index.md index 7a19d1b6b2c..58871eac93b 100644 --- a/src/contents/docs/15.how-to-guides/subflow-executions/index.md +++ b/src/contents/docs/15.how-to-guides/subflow-executions/index.md @@ -61,7 +61,7 @@ tasks: - id: fail type: io.kestra.plugin.core.execution.Fail - when: "{{ randomInt(lower=0, upper=2) == 1 }}" + runIf: "{{ randomInt(lower=0, upper=2) == 1 }}" errorMessage: Bad value returned! - id: end diff --git a/src/contents/docs/use-cases/03.microservices/index.md b/src/contents/docs/use-cases/03.microservices/index.md index 965d2a508a9..3ff22200811 100644 --- a/src/contents/docs/use-cases/03.microservices/index.md +++ b/src/contents/docs/use-cases/03.microservices/index.md @@ -58,25 +58,25 @@ tasks: - id: processPayment type: io.kestra.plugin.core.http.Request - when: "{{ outputs.checkInventory.code == 201 }}" + runIf: "{{ outputs.checkInventory.code == 201 }}" description: Process payment for the order uri: https://kestra.io/api/mock - id: orderConfirmation type: io.kestra.plugin.core.http.Request - when: "{{ outputs.processPayment.code == 201 }}" + runIf: "{{ outputs.processPayment.code == 201 }}" description: Confirm the order and notify the customer uri: https://kestra.io/api/mock - id: arrangeShipping type: io.kestra.plugin.core.http.Request - when: "{{ outputs.orderConfirmation.code == 201 }}" + runIf: "{{ outputs.orderConfirmation.code == 201 }}" description: Arrange shipping for the order uri: https://kestra.io/api/mock - id: updateDeliveryStatus type: io.kestra.plugin.core.http.Request - when: "{{ outputs.arrangeShipping.code == 201 }}" + runIf: "{{ outputs.arrangeShipping.code == 201 }}" description: Update the delivery status of the order uri: https://kestra.io/api/mock From 9e21b3000c9e6f3c904b21b72389f458e349cf13 Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Mon, 11 May 2026 13:01:29 +0200 Subject: [PATCH 39/52] docs(loop): update output exp Part of https://github.com/kestra-io/kestra-ee/issues/7644 --- .../01.tasks/00.flowable-tasks/index.md | 10 +++---- .../v2.0.0/foreach-loop/index.md | 10 +++---- .../04.functions/04.workflow/index.mdx | 26 ++++++++++++++++++- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/contents/docs/05.workflow-components/01.tasks/00.flowable-tasks/index.md b/src/contents/docs/05.workflow-components/01.tasks/00.flowable-tasks/index.md index 55a3a9434f4..3dfa7538be9 100644 --- a/src/contents/docs/05.workflow-components/01.tasks/00.flowable-tasks/index.md +++ b/src/contents/docs/05.workflow-components/01.tasks/00.flowable-tasks/index.md @@ -286,7 +286,7 @@ Read more about performance optimization in our [best practices guides](../../.. ### Loop -The `Loop` task iterates over a set of values and runs a set of child tasks for each item. Unlike `ForEach`, each iteration runs in an isolated sub-execution with its own context. +The `Loop` task iterates over a set of values and runs child tasks for each item. Unlike `ForEach`, each iteration runs in an isolated sub-execution with its own context. `values` accepts a list, a JSON array string, a map, or an ION file URI. When `values` is a URI, Kestra performs one iteration per line of the file. @@ -448,11 +448,11 @@ tasks: iteration_count = {{ outputs.process_items.iterationCount }} - # outputs.process_items.outputs is a map keyed by iteration value string: - # {"1": {"squared": 1}, "2": {"squared": 4}, "3": {"squared": 9}, ...} + # outputs.process_items.outputs is a list of iteration results: + # [{"item": {"value": "1", "iteration": 1}, "outputs": {"squared": 1}}, ...] all_outputs = {{ outputs.process_items.outputs | toJson }} - squared_values = [v["squared"] for v in all_outputs.values()] + squared_values = [iteration["outputs"]["squared"] for iteration in all_outputs] print(f"Processed {iteration_count} items") print(f"Squared values: {squared_values}") @@ -461,7 +461,7 @@ tasks: Kestra.outputs({"total": sum(squared_values)}) ``` -`outputs.process_items.iterationCount` is always available after the loop finishes. `outputs.process_items.outputs` is a map keyed by iteration value string — for `values: [1, 2, 3, 4, 5]`, the keys are `"1"`, `"2"`, `"3"`, `"4"`, `"5"`. To access a single iteration's output directly in an expression, use `outputs.process_items.outputs['1'].squared`. +`outputs.process_items.iterationCount` is always available after the loop finishes. `outputs.process_items.outputs` is a list of iteration results — each entry contains an `item` object (with `value`, `iteration`, and `key`) and an `outputs` map of the declared output values. To access the first iteration's output in an expression, use `outputs.process_items.outputs[0].outputs.squared`. To extract one output across all iterations as a list, use the `loopOutputs()` function: `{{ loopOutputs(outputs.process_items.outputs, 'squared') }}`. For more details, see the [Loop task documentation](/plugins/core/flow/io.kestra.plugin.core.flow.loop). diff --git a/src/contents/docs/11.migration-guide/v2.0.0/foreach-loop/index.md b/src/contents/docs/11.migration-guide/v2.0.0/foreach-loop/index.md index 4f60b7a9117..386ac05a527 100644 --- a/src/contents/docs/11.migration-guide/v2.0.0/foreach-loop/index.md +++ b/src/contents/docs/11.migration-guide/v2.0.0/foreach-loop/index.md @@ -37,7 +37,7 @@ The table below maps every `ForEach` expression to its `Loop` equivalent. The se | `parents[0].taskrun.value` | `item.parent.value` | Inside the inner of two nested loops | | `parents[1].taskrun.value` | `item.parents[0].value` | One level further up | | `outputs.task_id[taskrun.value].value` | `outputs.task_id.value` | Inside the iteration; task outputs are scoped to the current sub-execution | -| `outputs.foreach_id[value].field` (after the loop) | `outputs.loop_id.outputs['value'].output_id` (after the loop) | Outside the loop; collected outputs are keyed by iteration value string | +| `outputs.foreach_id[value].field` (after the loop) | `outputs.loop_id.outputs[n].outputs.output_id` (by index) or `loopOutputs(outputs.loop_id.outputs, 'output_id')` (all values as a list) | Outside the loop; outputs are now a list — key-based access by value string is no longer supported | ## Migrating `ForEach` @@ -259,7 +259,7 @@ tasks: In `ForEach`, task outputs from all iterations were automatically merged into a single map in the parent execution, keyed by `taskrun.value`. Any task after the loop could access `outputs.task_id[value].field` without any extra configuration. -In `Loop`, each iteration runs in its own sub-execution. Task outputs inside an iteration are **not** visible outside the loop by default. You must explicitly declare which values to expose using the `outputs` property on the Loop task. After the loop completes, the collected outputs are available as a map keyed by iteration value: `outputs..outputs[''].`. +In `Loop`, each iteration runs in its own sub-execution. Task outputs inside an iteration are **not** visible outside the loop by default. You must explicitly declare which values to expose using the `outputs` property on the Loop task. After the loop completes, `outputs..outputs` is a list of iteration results — each entry has an `item` object (with `value`, `iteration`, and `key`) and an `outputs` map of the declared output values. Access a single iteration by index: `outputs..outputs[n].outputs.`. To extract one output across all iterations as a list, use the [`loopOutputs()` function](../../../expressions/04.functions/04.workflow/index.mdx#loopoutputs). **Before** @@ -298,8 +298,8 @@ tasks: type: io.kestra.plugin.core.log.Log message: | Loop ran {{ outputs.loop.iterationCount }} iterations. - All results: {{ outputs.loop.outputs | toJson }} - Result for 'a': {{ outputs.loop.outputs['a'].result }} + All results: {{ loopOutputs(outputs.loop.outputs, 'result') }} + First result: {{ outputs.loop.outputs[0].outputs.result }} ``` ## Migrating `ForEachItem` @@ -345,6 +345,6 @@ For flows that relied on batch sizes larger than 1, you will need to restructure 2. Replace every `{{ taskrun.value }}` inside the loop with `{{ item.value }}`. 3. Replace every `{{ taskrun.iteration }}` inside the loop with `{{ item.index }}`. 4. Remove `parent.taskrun.value` and `parents[0].taskrun.value` references inside nested flowables — `{{ item.value }}` works directly at any nesting depth. -5. Update post-loop output access: declare `outputs` on the Loop task, then access collected results via `outputs..outputs[''].`. The map is keyed by iteration value string. +5. Update post-loop output access: declare `outputs` on the Loop task. After the loop, `outputs..outputs` is a list of iteration results — each entry has an `item` (with `value`, `iteration`, and `key`) and an `outputs` map. Access a single iteration by index: `outputs..outputs[n].outputs.`. To extract one output across all iterations as a list, use `{{ loopOutputs(outputs..outputs, '') }}`. 6. Search all flows for `io.kestra.plugin.core.flow.ForEachItem` and migrate to `Loop` with a URI value as described above. 7. Validate each updated flow by saving it in the Kestra UI or via the API and confirming no parse errors. diff --git a/src/contents/docs/expressions/04.functions/04.workflow/index.mdx b/src/contents/docs/expressions/04.functions/04.workflow/index.mdx index 777bb45cf5a..e63ca802ca6 100644 --- a/src/contents/docs/expressions/04.functions/04.workflow/index.mdx +++ b/src/contents/docs/expressions/04.functions/04.workflow/index.mdx @@ -1,13 +1,37 @@ --- title: "Workflow Helper Functions in Kestra Expressions" h1: "Workflow Helper Functions" -description: Reference for Kestra's workflow and execution helper functions — errorLogs(), currentEachOutput(), tasksWithState(), iterationOutput(), parentOutput(), and appLink(). +description: Reference for Kestra's workflow and execution helper functions — loopOutputs(), errorLogs(), currentEachOutput(), tasksWithState(), iterationOutput(), parentOutput(), and appLink(). sidebarTitle: Workflow Functions icon: /src/contents/docs/icons/expression.svg --- This group is more situational, but it becomes valuable in complex flows where you need to inspect sibling results, build links back into Kestra, or summarize failures. +## `loopOutputs()` + +Extracts a named output from every iteration of a [Loop](../../../05.workflow-components/01.tasks/00.flowable-tasks/index.md#loop) task and returns the values as an ordered list. Use it after a `Loop` task to collect one output field from all iterations without manually traversing the outputs list. + +```twig +{{ loopOutputs(outputs.myLoop.outputs, 'result') }} +``` + +`outputs` must be the loop's `.outputs` list. `name` is the declared output ID to extract. The function returns one value per iteration in order, with `null` for iterations where the key is missing. + +For a `Loop` over `["a", "b", "c"]` with a declared output `result`: + +```twig +{{ loopOutputs(outputs.loop.outputs, 'result') }} +{# → ["processed a", "processed b", "processed c"] #} +``` + +To access a single iteration directly, use list index notation: + +```twig +{{ outputs.loop.outputs[0].outputs.result }} {# first iteration's output #} +{{ outputs.loop.outputs[0].item.value }} {# first iteration's value #} +``` + ## `errorLogs()` Prints all error logs from the current execution: From f428e1f8df383802a22eba59ae54a63802c90eb6 Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Fri, 15 May 2026 16:12:10 +0200 Subject: [PATCH 40/52] docs(regex): improve regex filter and timeout docs for 2.0 Expand regexMatch/regexReplace/regexExtract descriptions with return type details, and clarify that the regex timeout causes an immediate error rather than a hang. Also name the three regex filters explicitly in the filter index page. Co-Authored-By: Claude Sonnet 4.6 --- .../docs/05.workflow-components/05.inputs/index.md | 4 ++-- .../docs/configuration/05.security-and-secrets/index.md | 2 +- .../docs/expressions/03.filters/03.strings/index.mdx | 8 ++++++-- src/contents/docs/expressions/03.filters/index.mdx | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/contents/docs/05.workflow-components/05.inputs/index.md b/src/contents/docs/05.workflow-components/05.inputs/index.md index 076aca311ea..1d178289c90 100644 --- a/src/contents/docs/05.workflow-components/05.inputs/index.md +++ b/src/contents/docs/05.workflow-components/05.inputs/index.md @@ -197,8 +197,8 @@ Below is the list of available properties for all inputs regardless of their typ Kestra validates the `type` of each input. In addition to the type validation, some input types can be configured with validation rules that are enforced at execution time. -- `STRING`: A `validator` property allows the addition of a validation [regex](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/regex/Pattern.html). Validator patterns are subject to a 10-second timeout. The timeout is configurable via [`kestra.regex.timeout`](../../configuration/05.security-and-secrets/index.md#regex-timeout). -- `SECRET`: Supports the same `validator` regex property as `STRING`, with the same 10-second timeout applied before the value is encrypted. +- `STRING`: A `validator` property allows the addition of a validation [regex](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/regex/Pattern.html). Validator patterns are subject to a 10-second timeout; executions that exceed it are rejected with an error. The timeout is configurable via [`kestra.regex.timeout`](../../configuration/05.security-and-secrets/index.md#regex-timeout). +- `SECRET`: Supports the same `validator` regex property as `STRING`, with the same 10-second timeout applied before the value is encrypted. This ensures the secret is never stored if the pattern is unsafe. - `INT`: `min` and `max` define the allowed range. - `FLOAT`: `min` and `max` define the allowed range. - `DURATION`: `min` and `max` define the allowed range. diff --git a/src/contents/docs/configuration/05.security-and-secrets/index.md b/src/contents/docs/configuration/05.security-and-secrets/index.md index 1a26a912758..1c57321d1a8 100644 --- a/src/contents/docs/configuration/05.security-and-secrets/index.md +++ b/src/contents/docs/configuration/05.security-and-secrets/index.md @@ -394,7 +394,7 @@ Keep the external process manager timeout longer than Kestra's own termination g ## Regex timeout -Kestra protects worker threads from ReDoS (catastrophic backtracking) by enforcing a timeout on all regex operations. This applies to [Pebble expression filters](../../expressions/index.mdx) (`regexMatch`, `regexReplace`, `regexExtract`, `replace` with `regexp=true`) and to `validator` patterns on `STRING` and `SECRET` inputs. +Kestra protects worker threads from ReDoS (catastrophic backtracking) by enforcing a timeout on all regex operations. This applies to [Pebble expression filters](../../expressions/index.mdx) (`regexMatch`, `regexReplace`, `regexExtract`, `replace` with `regexp=true`) and to `validator` patterns on `STRING` and `SECRET` inputs. When a pattern exceeds the limit, the task fails immediately with a timeout error rather than hanging indefinitely. The default timeout is **10 seconds**. To change it, set `kestra.regex.timeout` in your configuration: diff --git a/src/contents/docs/expressions/03.filters/03.strings/index.mdx b/src/contents/docs/expressions/03.filters/03.strings/index.mdx index eae13dba313..fa4e67279a1 100644 --- a/src/contents/docs/expressions/03.filters/03.strings/index.mdx +++ b/src/contents/docs/expressions/03.filters/03.strings/index.mdx @@ -122,7 +122,11 @@ Escapes special characters in a string. The `type` argument controls which style ## Regex filters -`regexMatch(regex)` returns `true` if the input contains a substring matching the pattern. `regexReplace(regex, replacement)` replaces all matching substrings. `regexExtract(regex, group)` returns the first match or a specific capture group (`group` defaults to `0`; returns `null` if no match): +Three filters cover the most common regex operations: + +- `regexMatch(regex)` — returns `true` if the input contains a substring matching the pattern, `false` otherwise. +- `regexReplace(regex, replacement)` — replaces all non-overlapping matches. Use `$1`, `$2`, … to reference capture groups in the replacement. +- `regexExtract(regex, group)` — returns the first match or a specific capture group. `group` defaults to `0` (the whole match); returns `null` if there is no match. ```twig {{ "hello world" | regexMatch("w[a-z]+") }} @@ -136,7 +140,7 @@ Escapes special characters in a string. The `type` argument controls which style ``` :::alert{type="warning"} -Regex filter operations are subject to a **10-second timeout** to prevent ReDoS (catastrophic backtracking). If a pattern takes longer than the limit, the task fails with an error message. +Regex filter operations are subject to a **10-second timeout** to prevent ReDoS (catastrophic backtracking). If a pattern takes longer than the limit, the task fails with a timeout error. Patterns with nested quantifiers such as `(a+)+` applied to large inputs are most likely to trigger this. Use anchored, non-ambiguous patterns to avoid it. The timeout can be adjusted with [`kestra.regex.timeout`](../../../configuration/05.security-and-secrets/index.md#regex-timeout) in your Kestra configuration. ::: diff --git a/src/contents/docs/expressions/03.filters/index.mdx b/src/contents/docs/expressions/03.filters/index.mdx index 3583dab4693..d673208f47b 100644 --- a/src/contents/docs/expressions/03.filters/index.mdx +++ b/src/contents/docs/expressions/03.filters/index.mdx @@ -14,7 +14,7 @@ Use filters when you need to transform a value with the pipe syntax: `{{ value | - [JSON and structured data](./01.json/index.mdx) — `toJson`, `toIon`, `jq` - [Numbers and collections](./02.collections/index.mdx) — `abs`, `number`, `first`, `last`, `sort`, `chunk`, `distinct`, and more -- [Strings](./03.strings/index.mdx) — `lower`, `upper`, `replace`, `slugify`, `base64encode`, regex filters, and more +- [Strings](./03.strings/index.mdx) — `lower`, `upper`, `replace`, `slugify`, `base64encode`, `regexMatch`, `regexReplace`, `regexExtract`, and more - [Dates](./04.dates/index.mdx) — `date`, `dateAdd`, `timestamp`, `timestampMilli`, and precision variants - [YAML](./05.yaml/index.mdx) — `yaml`, `indent`, `nindent` From 5f4011e3f7ce149d1c6718dc95767d7328341d74 Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Mon, 18 May 2026 19:13:22 +0200 Subject: [PATCH 41/52] docs(loop): major updates after enablement session --- .../docs/03.tutorial/05.flowable/index.md | 30 +- .../01.tasks/00.flowable-tasks/index.md | 310 +++++++++--------- .../01.tasks/02.taskruns/index.md | 120 +------ .../06.outputs/index.md | 151 +++------ .../v2.0.0/foreach-loop/index.md | 135 +++++++- .../11.foreach-and-foreachitem/index.md | 284 ---------------- .../docs/14.best-practices/11.loop/index.md | 230 +++++++++++++ .../docs/15.how-to-guides/loop/index.md | 133 +++++--- .../docs/expressions/01.context/index.mdx | 2 - src/contents/redirects/docs.yml | 2 + 10 files changed, 669 insertions(+), 728 deletions(-) delete mode 100644 src/contents/docs/14.best-practices/11.foreach-and-foreachitem/index.md create mode 100644 src/contents/docs/14.best-practices/11.loop/index.md diff --git a/src/contents/docs/03.tutorial/05.flowable/index.md b/src/contents/docs/03.tutorial/05.flowable/index.md index 86c8fdd5f18..fb2d937a99c 100644 --- a/src/contents/docs/03.tutorial/05.flowable/index.md +++ b/src/contents/docs/03.tutorial/05.flowable/index.md @@ -87,31 +87,27 @@ Execute the flow twice, once with `beauty` and once with `notebooks` to examine A common orchestration pattern is operating on a set of values. Kestra offers several approaches depending on your use case. The standalone examples below demonstrate each type. -### ForEach +### Loop -The **ForEach** flowable task executes a group of tasks for each value in the list. There are many ways to implement ForEach for complex looping operations, possibly incorporating conditional flowable tasks or subtasks. See more examples in the [ForEach documentation](/plugins/core/flow/io.kestra.plugin.core.flow.foreach). +The `Loop` flowable task iterates over a list of values and runs child tasks for each item. Each iteration runs as an isolated sub-execution. Access the current value with `{{ item.value }}` and the zero-based index with `{{ item.index }}`. -As an introduction to the feature, the below example demonstrates using ForEach to make an API call to [OpenLibrary](https://openlibrary.org/dev/docs/api/search) to get a list of associated titles for each author in the list. The values are defined as a JSON string or an array, i.e., a list of string values `["value1", "value2"]` or a list of key-value pairs `[{"key": "value1"}, {"key": "value2"}]`. - -You can access the current iteration value using the variable `{{ taskrun.value }}`: +Values can be a static list, a JSON array string, a map, or an ION file URI. The example below makes an API call for each author in the list: ```yaml -id: for_loop_example +id: loop_example namespace: tutorial tasks: - - id: for_each - type: io.kestra.plugin.core.flow.ForEach + - id: loop + type: io.kestra.plugin.core.flow.Loop values: ["pynchon", "dostoyevsky", "hedayat"] tasks: - id: api type: io.kestra.plugin.core.http.Request - uri: "https://openlibrary.org/search.json?author={{ taskrun.value }}&sort=new" + uri: "https://openlibrary.org/search.json?author={{ item.value }}&sort=new" ``` -After execution, the Gantt view shows separate runs for each of the three listed authors in the task. - -![forEach example](./for-each-author.png) +After execution, the Gantt view shows a separate task group for each author. See the [Loop documentation](../../05.workflow-components/01.tasks/00.flowable-tasks/index.md#loop) for output collection, nested loops, error handling, and map-reduce patterns. ### LoopUntil @@ -147,11 +143,11 @@ This flow checks an HTTP endpoint every 30 seconds and stops either when it retu A common orchestration requirement is executing independent processes **in parallel**. For example, you can process data for each partition in parallel. This can significantly speed up the processing time. -The flow below uses the `ForEach` flowable task to execute a list of `tasks` in parallel. +The flow below uses the `Loop` flowable task with `concurrencyLimit: 0` to process all partitions simultaneously. -1. The `concurrencyLimit` property with value `0` makes the list of `tasks` to execute in parallel. +1. The `concurrencyLimit` property set to `0` removes the cap on parallel iterations. 2. The `values` property defines the list of items to iterate over. -3. The `tasks` property defines the list of tasks to execute for each item in the list. You can access the iteration value using the `{{ taskrun.value }}` variable. +3. The `tasks` property defines the child tasks for each iteration. Access the iteration value with `{{ item.value }}`. ```yaml id: python_partitions @@ -171,7 +167,7 @@ tasks: Kestra.outputs({'partitions': partitions}) - id: processPartitions - type: io.kestra.plugin.core.flow.ForEach + type: io.kestra.plugin.core.flow.Loop concurrencyLimit: 0 values: '{{ outputs.getPartitions.vars.partitions }}' tasks: @@ -186,7 +182,7 @@ tasks: import time from kestra import Kestra - filename = '{{ taskrun.value }}' + filename = '{{ item.value }}' print(f"Reading and processing partition {filename}") nr_rows = random.randint(1, 1000) processing_time = random.randint(1, 20) diff --git a/src/contents/docs/05.workflow-components/01.tasks/00.flowable-tasks/index.md b/src/contents/docs/05.workflow-components/01.tasks/00.flowable-tasks/index.md index 3dfa7538be9..bc5bbbaac8e 100644 --- a/src/contents/docs/05.workflow-components/01.tasks/00.flowable-tasks/index.md +++ b/src/contents/docs/05.workflow-components/01.tasks/00.flowable-tasks/index.md @@ -140,150 +140,6 @@ tasks: For more details, check out the [If Task documentation](/plugins/core/flow/io.kestra.plugin.core.flow.if). -### ForEach - -:::alert{type="warning"} -`ForEach` is removed in Kestra 2.0. Use the [`Loop` task](#loop) instead. See the [ForEach → Loop migration guide](../../../11.migration-guide/v2.0.0/foreach-loop/index.md). -::: - -This task executes a group of tasks for each value in the list. - -In the following example, the variable is static, but it could also be generated from a previous task output, starting any number of subtasks. - -```yaml -id: foreach_example -namespace: company.team - -tasks: - - id: for_each - type: io.kestra.plugin.core.flow.ForEach - values: ["value 1", "value 2", "value 3"] - tasks: - - id: before_if - type: io.kestra.plugin.core.debug.Return - format: "Before if {{ taskrun.value }}" - - id: if - type: io.kestra.plugin.core.flow.If - condition: '{{ taskrun.value == "value 2" }}' - then: - - id: after_if - type: io.kestra.plugin.core.debug.Return - format: "After if {{ parent.taskrun.value }}" -``` - -In this execution, you can access: - -- The iteration value i.e., the index of a loop (the loop index starts at 0) using the syntax `{{ taskrun.iteration }}` -- The output of a sibling task using the syntax `{{ outputs.sibling[taskrun.value].value }}` -- The output from a previous iteration using `iterationOutput()`, for example `{{ iterationOutput() }}` for the same task or `{{ iterationOutput('taskId', taskrun.iteration - 1) }}` for a sibling task in an earlier iteration - -This example shows how to run tasks in parallel for each value in the list. All child tasks of the parallel task run in parallel. However, due to the `concurrencyLimit` property set to 2, only two parallel task groups run at any given time. - -```yaml -id: parallel_tasks_example -namespace: company.team - -tasks: - - id: for_each - type: io.kestra.plugin.core.flow.ForEach - values: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - concurrencyLimit: 2 - tasks: - - id: parallel - type: io.kestra.plugin.core.flow.Parallel - tasks: - - id: log - type: io.kestra.plugin.core.log.Log - message: Processing {{ parent.taskrun.value }} - - id: shell - type: io.kestra.plugin.scripts.shell.Commands - commands: - - sleep {{ parent.taskrun.value }} -``` - -For more information on handling outputs generated from `ForEach`, check out the [dedicated loop how-to guide](../../../15.how-to-guides/loop/index.md) and the [Best Practices for ForEach and ForEachItem](../../../14.best-practices/11.foreach-and-foreachitem/index.md) guide, including how to access [sibling task outputs correctly](../../../14.best-practices/11.foreach-and-foreachitem/index.md#example-use-sibling-outputs-correctly-inside-foreach) inside the loop. - -For processing items, or forwarding processing to a subflow, [ForEachItem](#foreachitem) is better suited. - -:::alert{type="info"} -For more details, refer to the [ForEach Task documentation](/plugins/core/flow/io.kestra.plugin.core.flow.foreach). -::: - -### ForEachItem - -:::alert{type="warning"} -`ForEachItem` is removed in Kestra 2.0. Use the [`Loop` task](#loop) with a URI value instead. See the [ForEach → Loop migration guide](../../../11.migration-guide/v2.0.0/foreach-loop/index.md). -::: - -This task iterates over a list of items and runs a subflow for each item, or for each batch of items. - -```yaml - - id: each - type: io.kestra.plugin.core.flow.ForEachItem - items: "{{ inputs.file }}" # could be also an output variable {{ outputs.extract.uri }} - inputs: - file: "{{ taskrun.items }}" # items of the batch - batch: - rows: 4 - namespace: company.team - flowId: subflow - revision: 1 # optional (default: latest) - wait: true # wait for the subflow execution - transmitFailed: true # fail the task run if the subflow execution fails - labels: # optional labels to pass to the subflow to be executed - key: value -``` - -This executes the subflow `company.team.subflow` for each batch of items. -To pass the batch of items to a subflow, you can use inputs. The example above uses an input of `FILE` type called `file` that takes the URI of an internal storage file containing the batch of items. - -The next example shows you how to access the outputs from each subflow executed. The ForEachItem automatically merges the URIs of the outputs from each subflow into a single file. The URI of this file is available through the `subflowOutputs` output. - -```yaml -id: for_each_item -namespace: company.team - -tasks: - - id: generate - type: io.kestra.plugin.scripts.shell.Script - script: | - for i in $(seq 1 10); do echo "$i" >> data; done - outputFiles: - - data - - - id: for_each_item - type: io.kestra.plugin.core.flow.ForEachItem - items: "{{ outputs.generate.outputFiles.data }}" - batch: - rows: 4 - wait: true - flowId: my_subflow - namespace: company.team - inputs: - value: "{{ taskrun.items }}" - - - id: for_each_outputs - type: io.kestra.plugin.core.log.Log - message: "{{ outputs.forEachItem_merge.subflowOutputs }}" # Log the URI of the file containing the URIs of the outputs from each subflow -``` - -:::alert{type="info"} -For more details, refer to the [ForEachItem Task documentation](/plugins/core/flow/io.kestra.plugin.core.flow.foreachitem). -::: - -#### `ForEach` vs `ForEachItem` - -Both `ForEach` and `ForEachItem` are similar, but there are specific use cases that suit one over the other: -- `ForEach` generates a lot of [Task Runs](../02.taskruns/index.md) which can impact performance. -- `ForEachItem` generates separate executions using [Subflows](../../10.subflows/index.md) for the group of tasks. This scales better for larger datasets. - -Read more about performance optimization in our [best practices guides](../../../14.best-practices/0.flows/index.md#tasks-in-the-same-execution). - -
- -
- - ### Loop The `Loop` task iterates over a set of values and runs child tasks for each item. Unlike `ForEach`, each iteration runs in an isolated sub-execution with its own context. @@ -317,6 +173,31 @@ Inside each iteration, use the `item` variable to access the iteration context: For more details on `item`, see [loop iteration context](../../../expressions/index.mdx#loop-iteration-context) in the expressions reference. +#### Iterating over objects + +When `values` contains a list of objects, each `item.value` is a JSON string. Use `fromJson(item.value).field` to read fields — `item.value.field` does not work. + +```yaml +tasks: + - id: loop + type: io.kestra.plugin.core.flow.Loop + values: + - { id: 101, email: "a@example.com" } + - { id: 102, email: "b@example.com" } + fetchType: AUTO + outputs: + - id: user_id + type: INT + value: "{{ fromJson(item.value).id }}" + - id: email + type: STRING + value: "{{ fromJson(item.value).email }}" + tasks: + - id: log_user + type: io.kestra.plugin.core.log.Log + message: "User {{ fromJson(item.value).id }} -> {{ fromJson(item.value).email }}" +``` + #### Concurrent execution By default (`concurrencyLimit: 1`), iterations run one at a time in order. Set `concurrencyLimit` to a higher value to run multiple iterations simultaneously, or `0` for no limit. @@ -325,12 +206,19 @@ By default (`concurrencyLimit: 1`), iterations run one at a time in order. Set ` tasks: - id: loop type: io.kestra.plugin.core.flow.Loop - values: [1, 2, 3, 4, 5] - concurrencyLimit: 3 + values: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + concurrencyLimit: 0 tasks: - - id: log - type: io.kestra.plugin.core.log.Log - message: "Processing {{ item.value }} (index={{ item.index }})" + - id: parallel + type: io.kestra.plugin.core.flow.Parallel + tasks: + - id: log + type: io.kestra.plugin.core.log.Log + message: "Processing {{ item.value }}" + - id: shell + type: io.kestra.plugin.scripts.shell.Commands + commands: + - "echo done {{ item.value }}" ``` #### Failure propagation @@ -356,27 +244,64 @@ tasks: message: "OK: {{ item.value }}" ``` +#### Error handling per iteration + +Use `errors:` to run tasks when an iteration fails, and `finally:` to run a block once after all iterations complete regardless of outcome. `errors:` requires `transmitFailed: false` — with the default `transmitFailed: true`, a failed iteration stops the loop before `errors:` can run. `finally:` always runs regardless of the `transmitFailed` setting. + +```yaml +tasks: + - id: loop + type: io.kestra.plugin.core.flow.Loop + values: + - ok + - boom + - ok + transmitFailed: false + tasks: + - id: maybe_fail + type: io.kestra.plugin.scripts.shell.Commands + commands: + - | + if [ "{{ item.value }}" = "boom" ]; then + echo "failing on {{ item.value }}" >&2 + exit 1 + fi + echo "ok {{ item.value }}" + errors: + - id: handle_error + type: io.kestra.plugin.core.log.Log + message: "Iteration {{ item.index }} ({{ item.value }}) failed" + finally: + - id: cleanup + type: io.kestra.plugin.core.log.Log + message: "Loop completed (with or without failures)" +``` + #### Nested loops Loops can be nested to any depth. Because `item` is bound to the loop execution rather than individual task runs, flowable tasks nested inside a loop can access `item` directly without a `parent.` prefix. +`item.parents[0]` is the immediate parent loop (same as `item.parent`), `item.parents[1]` is the next outer loop, and so on. + ```yaml tasks: - id: outer type: io.kestra.plugin.core.flow.Loop - values: [1, 2, 3] + values: ["bucket1", "bucket2"] tasks: - - id: inner + - id: middle type: io.kestra.plugin.core.flow.Loop - values: ["a", "b"] + values: [2025, 2026] tasks: - - id: log - type: io.kestra.plugin.core.log.Log - message: "outer={{ item.parent.value }} inner={{ item.value }}" + - id: inner + type: io.kestra.plugin.core.flow.Loop + values: ["Jan", "Feb", "Mar"] + tasks: + - id: log + type: io.kestra.plugin.core.log.Log + message: "bucket={{ item.parents[1].value }} year={{ item.parent.value }} month={{ item.value }}" ``` -For deeper hierarchies, `item.parents[0]` is the immediate parent loop, `item.parents[1]` is the next outer loop, and so on. - #### Loop outputs By default, task outputs produced inside a loop are not accessible to tasks that run after the loop. Use the `outputs` property on the Loop task to explicitly declare which values to expose. @@ -389,6 +314,7 @@ tasks: - id: loop type: io.kestra.plugin.core.flow.Loop values: ["a", "b", "c"] + fetchType: AUTO outputs: - id: result type: STRING @@ -411,7 +337,75 @@ The loop also exposes monitoring outputs regardless of whether `outputs` is decl | `runningIterations` | Iterations still in progress | | `terminatedIterations` | Iterations that have finished | -The `fetchType` property controls how iteration outputs are collected: `FETCH` returns them directly in the execution context, `STORE` writes them to internal storage as a URI, and `AUTO` (the default) chooses based on whether `values` is a URI. +The `fetchType` property controls how iteration outputs are collected: `FETCH` returns them inline in the execution context (suitable for small iteration counts), `STORE` writes them to internal storage and exposes a URI (preferred for large iteration counts), and `AUTO` (the default) chooses based on whether `values` is a URI. + +#### Processing large files + +When `values` is a list of URIs from a [`Split`](/plugins/core/storage/io.kestra.plugin.core.storage.split) task, each iteration receives one chunk URI as `item.value`. Combine `Split`, `Loop`, and `Concat` to implement a map-reduce pattern: split a large file into chunks, process each chunk in parallel, then merge the per-chunk outputs into a single result. + +Passing `values: "{{ outputs.split.uris }}"` where `outputs.split.uris` is a **list** is different from passing a single file URI. When `values` is a list, each `item.value` is one element of that list. When `values` is a single URI string, Kestra iterates line-by-line through the file. + +```yaml +id: map-reduce +namespace: company.team + +tasks: + - id: download + type: io.kestra.plugin.core.http.Download + uri: https://huggingface.co/datasets/kestra/datasets/raw/main/csv/orders.csv + + - id: to_ion + type: io.kestra.plugin.serdes.csv.CsvToIon + from: "{{ outputs.download.uri }}" + + - id: split + type: io.kestra.plugin.core.storage.Split + from: "{{ outputs.to_ion.uri }}" + rows: 25 + + - id: per_chunk + type: io.kestra.plugin.core.flow.Loop + values: "{{ outputs.split.uris }}" + concurrencyLimit: 4 + fetchType: FETCH + outputs: + - id: data + type: STRING + value: "{{ outputs.aggregate.uri }}" + tasks: + - id: aggregate + type: io.kestra.plugin.transform.Aggregate + from: "{{ item.value }}" + outputType: STORE + groupBy: [customer_email] + aggregates: + orders: + expr: count() + type: INT + revenue: + expr: sum(todecimal(total)) + type: DECIMAL + + - id: concat + type: io.kestra.plugin.core.storage.Concat + files: "{{ loopOutputs(outputs.per_chunk.outputs, 'data') }}" + extension: .ion + + - id: reduce + type: io.kestra.plugin.transform.Aggregate + from: "{{ outputs.concat.uri }}" + outputType: STORE + groupBy: [data.customer_email] + aggregates: + orders: + expr: sum(orders) + type: INT + revenue: + expr: sum(revenue) + type: DECIMAL +``` + +Use `fetchType: FETCH` to collect per-iteration output URIs inline, then pass them to `Concat` via `loopOutputs(outputs.per_chunk.outputs, 'data')`. #### Accessing loop outputs in a script task diff --git a/src/contents/docs/05.workflow-components/01.tasks/02.taskruns/index.md b/src/contents/docs/05.workflow-components/01.tasks/02.taskruns/index.md index 621ea085bd0..7dbab795105 100644 --- a/src/contents/docs/05.workflow-components/01.tasks/02.taskruns/index.md +++ b/src/contents/docs/05.workflow-components/01.tasks/02.taskruns/index.md @@ -70,125 +70,33 @@ The logs show the following: } ``` -## Task run values +## Loop iteration context -Some [Flowable tasks](../00.flowable-tasks/index.md), such as [ForEach](../00.flowable-tasks/index.md) and [ForEachItem](../00.flowable-tasks/index.md#foreachitem), group tasks together. You can use `{{ taskrun.value }}` to access the value of a specific task run. - -In the example below, `foreach` iterates twice over the values `[1, 2]`: - -```yaml -id: loop -namespace: company.team - -tasks: - - id: foreach - type: io.kestra.plugin.core.flow.ForEach - values: [1, 2] - tasks: - - id: log - type: io.kestra.plugin.core.log.Log - message: - - "{{ taskrun }}" - - "{{ taskrun.value }}" - - "{{ taskrun.id }}" - - "{{ taskrun.startDate }}" - - "{{ taskrun.attemptsCount }}" - - "{{ taskrun.parentId }}" - - "{{ taskrun.iteration }}" -``` -This produces two separate log entries, one with `1` and the other with `2`. - -In `ForEach`, `taskrun.iteration` is often paired with `iterationOutput()` when you need to read a previous iteration's result. For example, `{{ iterationOutput('my_task', taskrun.iteration - 1) }}` fetches the output of `my_task` from the previous loop index. - -### 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: +Inside a [Loop](../00.flowable-tasks/index.md#loop) task, each iteration runs as an isolated sub-execution. Use `{{ item.value }}` and `{{ item.index }}` to access the current iteration value and zero-based index from any task inside that sub-execution, including tasks nested inside `If`, `Parallel`, or other flowable tasks. ```yaml id: loop namespace: company.team tasks: - - id: foreach - type: io.kestra.plugin.core.flow.ForEach - values: [1, 2] + - id: loop + type: io.kestra.plugin.core.flow.Loop + values: [1, 2, 3] tasks: - id: log type: io.kestra.plugin.core.log.Log - message: "{{ taskrun.value }}" - - id: if - type: io.kestra.plugin.core.flow.If - condition: "{{ true }}" - then: - - id: log_parent - type: io.kestra.plugin.core.log.Log - message: "{{ parent.taskrun.value }}" + message: | + value={{ item.value }} + index={{ item.index }} + taskrun.id={{ taskrun.id }} + taskrun.startDate={{ taskrun.startDate }} + taskrun.attemptsCount={{ taskrun.attemptsCount }} + taskrun.parentId={{ taskrun.parentId }} ``` -This iterates through the `log` and `if` tasks twice as there are two items in `values` property. The `log_parent` task logs the parent task run value as `1` and then `2`. - -### Parent vs. parents in nested Flowable tasks - -With nested [Flowable tasks](../00.flowable-tasks/index.md), only the immediate parent is available through `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 }}" -``` +For nested loops, `{{ item.parent.value }}` accesses the immediate enclosing loop's value, and `{{ item.parents[n].value }}` accesses deeper ancestors (`[0]` = immediate parent, `[1]` = grandparent, and so on). -The `parent` variable gives direct access to the first parent, while the `parents[INDEX]` gives you access to the parent higher up the tree. +See [Loop iteration context](../../../expressions/01.context/index.mdx#loop-iteration-context) in the expressions reference for the full `item` variable table. :::collapse{title="Task Run JSON Object Example"} ```json diff --git a/src/contents/docs/05.workflow-components/06.outputs/index.md b/src/contents/docs/05.workflow-components/06.outputs/index.md index 30c2b32cb86..ce3f08f3da1 100644 --- a/src/contents/docs/05.workflow-components/06.outputs/index.md +++ b/src/contents/docs/05.workflow-components/06.outputs/index.md @@ -200,195 +200,134 @@ outputs: Note how the Ternary Operator `{{ condition ? value_if_true : value_if_false }}` is used in the output expression `{{ tasks.main.state != 'SKIPPED' ? outputs.main.value : outputs.fallback.value }}` to return the output of the `main` task if it is not skipped, otherwise, it returns the output of the `fallback` task. -## Dynamic variables (Each tasks) +## Loop outputs and iteration context -### Current taskrun value +### Current iteration value -In dynamic flows (for example, with an **Each** loop), variables are passed to tasks dynamically. You can access the current taskrun value with `{{ taskrun.value }}` like this: +Inside a [Loop](../01.tasks/00.flowable-tasks/index.md#loop) task, each iteration runs as an isolated sub-execution. Use `{{ item.value }}` to access the current value and `{{ item.index }}` for the zero-based position. ```yaml -id: taskrun_value_example +id: loop_value_example namespace: company.team tasks: - - id: each - type: io.kestra.plugin.core.flow.ForEach + - id: loop + type: io.kestra.plugin.core.flow.Loop values: ["alpha", "beta", "gamma"] tasks: - id: inner type: io.kestra.plugin.core.debug.Return - format: "{{ task.id }} > {{ taskrun.value }} > {{ taskrun.startDate }}" + format: "{{ task.id }} > {{ item.value }} > {{ item.index }}" ``` -The **Outputs** tab contains the output for each of the inner task. - -![taskrun_value_example](./taskrun_value_example.png) +The **Outputs** tab shows the output for each iteration of the inner task. ### Loop over a list of JSON objects -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. +When `values` contains objects, each `item.value` is a JSON string. Use `fromJson(item.value).field` to access properties — `item.value.field` does not work. ```yaml -id: loop_sequentially_over_list +id: loop_json_objects namespace: company.team tasks: - - id: each - type: io.kestra.plugin.core.flow.ForEach + - id: loop + type: io.kestra.plugin.core.flow.Loop values: - {"key": "my-key", "value": "my-value"} - {"key": "my-complex", "value": {"sub": 1, "bool": true}} tasks: - id: inner type: io.kestra.plugin.core.debug.Return - format: "{{ fromJson(taskrun.value).key }} > {{ fromJson(taskrun.value).value }}" + format: "{{ fromJson(item.value).key }} > {{ fromJson(item.value).value }}" ``` +### Access outputs from loop iterations -### Specific outputs for dynamic tasks - -Dynamic tasks are tasks that run other tasks a certain number of times. A dynamic task runs multiple iterations of a set of sub-tasks. - -For example, **ForEach** produces other tasks dynamically depending on its `values` property. - -It is possible to reach each iteration output of dynamic tasks by using the following syntax: +By default, task outputs produced inside a loop are not visible to tasks that run after it. Declare an `outputs:` block on the Loop task to surface values explicitly. ```yaml -id: output_sample +id: loop_outputs namespace: company.team tasks: - - id: each - type: io.kestra.plugin.core.flow.ForEach + - id: loop + type: io.kestra.plugin.core.flow.Loop values: ["s1", "s2", "s3"] + fetchType: AUTO + outputs: + - id: result + type: STRING + value: "{{ outputs.sub.value }}" tasks: - id: sub type: io.kestra.plugin.core.debug.Return - format: "{{ task.id }} > {{ taskrun.value }} > {{ taskrun.startDate }}" + format: "{{ task.id }} > {{ item.value }}" - id: use type: io.kestra.plugin.core.debug.Return - format: "Previous task produced output: {{ outputs.sub.s1.value }}" + format: "First result: {{ outputs.loop.outputs[0].outputs.result }}" ``` -The `outputs.sub.s1.value` variable reaches the `value` of the `sub` task of the `s1` iteration. - -### Previous task lookup +After the loop, `outputs..outputs` is a list of per-iteration results — each entry has an `item` object (with `value`, `index`, and `key`) and an `outputs` map of the declared output values. -It is also possible to locate a specific dynamic task by its `value`: +- Access one iteration by index: `outputs..outputs[n].outputs.` +- Extract one field across all iterations as a list: `{{ loopOutputs(outputs..outputs, '') }}` -```yaml -id: dynamic_looping -namespace: company.team +### Sibling task outputs inside a loop -tasks: - - id: each - type: io.kestra.plugin.core.flow.ForEach - values: ["alpha", "beta", "gamma"] - tasks: - - id: inner - type: io.kestra.plugin.core.debug.Return - format: "{{ taskrun.value }}" - - - id: end - type: io.kestra.plugin.core.debug.Return - format: "{{ task.id }} > {{ outputs.inner['alpha'].value }}" -``` - -It uses the format `outputs.TASKID[VALUE].ATTRIBUTE`. The special bracket `[]` in `[VALUE]` is called the subscript notation; it enables using special chars like space or '-' in task identifiers or output attributes. - -### Lookup in sibling tasks - -Sometimes it is useful to access outputs from other tasks in the same task tree, known as sibling tasks. - -If the task tree is static, for example when using the [Sequential](/plugins/core/flow/io.kestra.plugin.core.flow.sequential) task, you can use the `{{ outputs.task_id.value }}` notation where `task_id` is the identifier of the sibling task, as you would outside of the task tree. - -For example: +Inside a Loop iteration, sibling task outputs are accessed with the plain `outputs.task_id.attribute` notation — each iteration runs in its own isolated sub-execution, so there is no ambiguity about which iteration's output you are reading. ```yaml -id: sibling_tasks +id: loop_with_sibling_tasks namespace: company.team tasks: - - id: sequential - type: io.kestra.plugin.core.flow.Sequential + - id: loop + type: io.kestra.plugin.core.flow.Loop + values: ["alpha", "beta", "gamma"] tasks: - id: first type: io.kestra.plugin.core.output.OutputValues values: - data: "hello from task 1" + data: "First value: {{ item.value }}" - id: second type: io.kestra.plugin.core.output.OutputValues values: data: "{{ outputs.first.values.data }}" - - - id: log_siblings - type: io.kestra.plugin.core.log.Log - message: "{{ outputs.second.values.data }}" ``` -If the task tree is dynamic, for example when using the [ForEach](/plugins/core/flow/io.kestra.plugin.core.flow.foreach) task, you need to use `{{ outputs.task_id[taskrun.value] }}` to access the current tree task. `taskrun.value` is a special variable that holds the current value of the ForEach task. - -For example: +For static task trees using [Sequential](/plugins/core/flow/io.kestra.plugin.core.flow.sequential), the same `{{ outputs.task_id.value }}` notation applies outside of a loop. ```yaml -id: loop_with_sibling_tasks +id: sibling_tasks namespace: company.team tasks: - - id: foreach - type: io.kestra.plugin.core.flow.ForEach - values: ["alpha", "beta", "gamma"] + - id: sequential + type: io.kestra.plugin.core.flow.Sequential tasks: - id: first type: io.kestra.plugin.core.output.OutputValues values: - data: "First value: {{ taskrun.value }}" + data: "hello from task 1" - id: second type: io.kestra.plugin.core.output.OutputValues values: - data: "{{ outputs.first[taskrun.value].values.data }}" + data: "{{ outputs.first.values.data }}" - - id: log_output_from_foreach + - id: log_siblings type: io.kestra.plugin.core.log.Log - message: "{{ outputs.second['alpha'].values.data }}" -``` - -You can also use the `currentEachOutput` function to access the current tree task. See [Function Reference](../../expressions/04.functions/index.mdx) for more details. - -If you need the output from a previous iteration of the same task, or from a sibling task in a previous iteration, use `iterationOutput()` instead. - -For example, this flow builds a running total by reading the previous iteration's output: - -```yaml -id: foreach_prefix_sum -namespace: company.team - -tasks: - - id: foreach - type: io.kestra.plugin.core.flow.ForEach - values: ["100", "200", "300"] - tasks: - - id: prefix_sum - type: io.kestra.plugin.core.debug.Return - format: >- - {% set idx = taskrun.iteration %} - {% if idx == 0 %} - {{ taskrun.value | trim | number }} - {% else %} - {{ iterationOutput('prefix_sum', idx - 1) | trim | number + taskrun.value | trim | number }} - {% endif %} + message: "{{ outputs.second.values.data }}" ``` -`iterationOutput()` defaults to the current task and the previous iteration, so `{{ iterationOutput() }}` is equivalent to reading the current task's output from `taskrun.iteration - 1`. - :::alert{type="warning"} -Accessing sibling task outputs is impossible on [Parallel](/plugins/core/flow/io.kestra.plugin.core.flow.parallel) as it runs tasks in parallel. +Accessing sibling task outputs is impossible in [Parallel](/plugins/core/flow/io.kestra.plugin.core.flow.parallel) as tasks run simultaneously. ::: -For more examples and guidance on accessing sibling outputs inside `ForEach`, including how to read them both inside and outside the loop, see [Best Practices for ForEach and ForEachItem](../../14.best-practices/11.foreach-and-foreachitem/index.md#example-use-sibling-outputs-correctly-inside-foreach). +For more output patterns including map-reduce and large-file processing, see [Loop best practices](../../14.best-practices/11.loop/index.md). ## Outputs preview diff --git a/src/contents/docs/11.migration-guide/v2.0.0/foreach-loop/index.md b/src/contents/docs/11.migration-guide/v2.0.0/foreach-loop/index.md index 386ac05a527..262faddcf14 100644 --- a/src/contents/docs/11.migration-guide/v2.0.0/foreach-loop/index.md +++ b/src/contents/docs/11.migration-guide/v2.0.0/foreach-loop/index.md @@ -33,12 +33,25 @@ The table below maps every `ForEach` expression to its `Loop` equivalent. The se |---|---|---| | `taskrun.value` | `item.value` | | | `taskrun.iteration` | `item.index` | Zero-based in both | -| `parent.taskrun.value` | `item.value` | No prefix needed inside nested flowables | -| `parents[0].taskrun.value` | `item.parent.value` | Inside the inner of two nested loops | -| `parents[1].taskrun.value` | `item.parents[0].value` | One level further up | +| `parent.taskrun.value` | `item.value` | No prefix needed — `item` is accessible from any depth inside a Loop sub-execution, including inside `If` or `Parallel` | +| `parents[0].taskrun.value` | `item.parent.value` | Only when inside an inner of two nested Loops; when used inside a nested flowable (If, Parallel) within a single Loop, it maps to `item.value` instead | +| `parents[1].taskrun.value` | `item.parents[1].value` | One level further up | | `outputs.task_id[taskrun.value].value` | `outputs.task_id.value` | Inside the iteration; task outputs are scoped to the current sub-execution | | `outputs.foreach_id[value].field` (after the loop) | `outputs.loop_id.outputs[n].outputs.output_id` (by index) or `loopOutputs(outputs.loop_id.outputs, 'output_id')` (all values as a list) | Outside the loop; outputs are now a list — key-based access by value string is no longer supported | +## Pattern quick reference + +The table below maps the common 1.0 iteration patterns to their 2.0 equivalents. The expression quick reference above covers the variable renames; this table covers the broader structural changes. + +| 1.0 pattern | 2.0 replacement | What changed | +|---|---|---| +| `ForEach` | `Loop` | Same shape. Adds typed `outputs:`, `finally:`, and `item.parents[N]`. | +| `ForEach + If` | `Loop + If` | No change to the inner `If` task — update expressions only. | +| `ForEachItem` | `Loop + Subflow` | Isolation is now opt-in via `Subflow`. Same per-batch execution model, explicit instead of implicit. | +| `subflowOutputs` | `outputs: [{id, type, value}]` | Per-iteration outputs are declared, not auto-surfaced. | +| `taskrun.value` / `taskrun.iteration` | `item.value` / `item.index` | Variable rename only. | +| Built-in batch aggregation | `Concat + Aggregate` | Reduce is its own task — compose explicitly after the loop. | + ## Migrating `ForEach` For most flows, the `ForEach` migration has three parts: replace the task type, update expressions inside child tasks, and update any post-loop output access. The sections below cover each pattern with before-and-after examples. @@ -255,11 +268,57 @@ tasks: message: "env={{ item.key }} url={{ item.value }}" ``` +### Iterating over object lists + +If your `ForEach` flow iterated over a list of objects using `fromJson(taskrun.value).field`, the same pattern applies in `Loop` — only the variable name changes. + +**Before** + +```yaml +tasks: + - id: for_each + type: io.kestra.plugin.core.flow.ForEach + values: + - { id: 101, email: "a@example.com" } + - { id: 102, email: "b@example.com" } + tasks: + - id: log + type: io.kestra.plugin.core.log.Log + message: "User {{ fromJson(taskrun.value).id }} -> {{ fromJson(taskrun.value).email }}" +``` + +**After** + +```yaml +tasks: + - id: loop + type: io.kestra.plugin.core.flow.Loop + values: + - { id: 101, email: "a@example.com" } + - { id: 102, email: "b@example.com" } + tasks: + - id: log + type: io.kestra.plugin.core.log.Log + message: "User {{ fromJson(item.value).id }} -> {{ fromJson(item.value).email }}" +``` + +`item.value` is always a string when list elements are not plain strings. Never access `item.value.field` directly — use `fromJson(item.value).field`. + ### Outputs In `ForEach`, task outputs from all iterations were automatically merged into a single map in the parent execution, keyed by `taskrun.value`. Any task after the loop could access `outputs.task_id[value].field` without any extra configuration. -In `Loop`, each iteration runs in its own sub-execution. Task outputs inside an iteration are **not** visible outside the loop by default. You must explicitly declare which values to expose using the `outputs` property on the Loop task. After the loop completes, `outputs..outputs` is a list of iteration results — each entry has an `item` object (with `value`, `iteration`, and `key`) and an `outputs` map of the declared output values. Access a single iteration by index: `outputs..outputs[n].outputs.`. To extract one output across all iterations as a list, use the [`loopOutputs()` function](../../../expressions/04.functions/04.workflow/index.mdx#loopoutputs). +In `Loop`, each iteration runs in its own sub-execution. Task outputs inside an iteration are **not** visible outside the loop by default. You must explicitly declare which values to expose using the `outputs` property on the Loop task, and set `fetchType` to control how they are stored and accessed downstream: + +| `fetchType` | Downstream access | When to use | +|---|---|---| +| `FETCH` | `outputs..outputs` — in-memory list of all iterations | Small iteration counts | +| `STORE` | `outputs..uri` — URI to a file in internal storage | Large iteration counts | +| `FETCH_ONE` | Last iteration's outputs only | When each pass overwrites the previous result | +| `AUTO` | Default; picks automatically based on whether `values` is a URI | General use | +| `NONE` | Outputs not surfaced | When you only care about side effects | + +With `FETCH`, after the loop completes, `outputs..outputs` is a list of iteration results — each entry has an `item` object (with `value`, `iteration`, and `key`) and an `outputs` map of the declared output values. Access a single iteration by index: `outputs..outputs[n].outputs.`. To extract one output across all iterations as a list, use the [`loopOutputs()` function](../../../expressions/04.functions/04.workflow/index.mdx#loopoutputs). With `STORE`, the results are written to internal storage and exposed as `outputs..uri` — use this for loops with large iteration counts where loading everything into memory is not practical. **Before** @@ -304,9 +363,11 @@ tasks: ## Migrating `ForEachItem` -`ForEachItem` dispatched each batch to a separate subflow execution. With `Loop`, you process each item inline — no separate flow required. When `values` is an internal storage URI, Kestra iterates over each line of the file and runs the child tasks for that line, with `item.value` holding the content of each line. +`ForEachItem` dispatched each batch to a separate subflow execution. `Loop` offers two migration paths depending on whether you need per-batch execution isolation. -For flows that used `ForEachItem` with `batch.rows: 1`, the migration is a direct substitution: replace the `ForEachItem` block and its subflow with a single `Loop` task with inline tasks. +### Inline processing + +For flows that used `ForEachItem` with `batch.rows: 1`, the migration is a direct substitution: replace the `ForEachItem` block and its subflow with a single `Loop` task with inline tasks. When `values` is an internal storage URI, `Loop` iterates one line per iteration with `item.value` holding the line content. **Before** @@ -337,7 +398,65 @@ tasks: message: "Processing: {{ item.value }}" ``` -For flows that relied on batch sizes larger than 1, you will need to restructure: either reduce to single-item processing, or keep a `Subflow` task inside a `Loop` and handle batching within that subflow. +### Isolated per-batch execution + +If your `ForEachItem` flows relied on subflow-level isolation — separate retries, separate logs, and failure containment per batch — preserve that isolation with `Loop` + `Subflow`. Split the source file into chunk URIs first, then loop over the URIs and call the child flow per batch. + +**Parent flow** + +```yaml +tasks: + - id: split + type: io.kestra.plugin.core.storage.Split + from: "{{ inputs.file }}" + rows: 100 + + - id: per_batch + type: io.kestra.plugin.core.flow.Loop + values: "{{ outputs.split.uris }}" + concurrencyLimit: 4 + fetchType: FETCH + outputs: + - id: result_uri + type: STRING + value: "{{ outputs.run_child.outputs.uri }}" + tasks: + - id: run_child + type: io.kestra.plugin.core.flow.Subflow + namespace: company.team + flowId: process_batch + wait: true + transmitFailed: true + inputs: + batch_uri: "{{ item.value }}" + + - id: concat + type: io.kestra.plugin.core.storage.Concat + files: "{{ loopOutputs(outputs.per_batch.outputs, 'result_uri') }}" + extension: .ion +``` + +**Child flow** (`process_batch`) + +```yaml +inputs: + - id: batch_uri + type: STRING + +tasks: + - id: process + type: io.kestra.plugin.core.debug.Return + format: "{{ inputs.batch_uri }}" + +outputs: + - id: uri + type: STRING + value: "{{ outputs.process.value }}" +``` + +The child flow surfaces its result through a flow-level `outputs:` declaration. The parent collects all result URIs with `loopOutputs(outputs.per_batch.outputs, 'result_uri')` and passes them to `Concat`. + +For flows that used `ForEachItem` with batch sizes larger than 1, set `rows` on the `Split` task to your batch size — each chunk URI passed to the child flow then contains that many rows. ## Migration steps @@ -345,6 +464,6 @@ For flows that relied on batch sizes larger than 1, you will need to restructure 2. Replace every `{{ taskrun.value }}` inside the loop with `{{ item.value }}`. 3. Replace every `{{ taskrun.iteration }}` inside the loop with `{{ item.index }}`. 4. Remove `parent.taskrun.value` and `parents[0].taskrun.value` references inside nested flowables — `{{ item.value }}` works directly at any nesting depth. -5. Update post-loop output access: declare `outputs` on the Loop task. After the loop, `outputs..outputs` is a list of iteration results — each entry has an `item` (with `value`, `iteration`, and `key`) and an `outputs` map. Access a single iteration by index: `outputs..outputs[n].outputs.`. To extract one output across all iterations as a list, use `{{ loopOutputs(outputs..outputs, '') }}`. +5. Update post-loop output access: declare `outputs` on the Loop task and set `fetchType`. With `FETCH`, `outputs..outputs` is an in-memory list of iteration results — access a single entry by index with `outputs..outputs[n].outputs.`, or extract one field across all iterations as a list with `{{ loopOutputs(outputs..outputs, '') }}`. With `STORE`, results are written to internal storage and exposed as `outputs..uri`. See the [fetchType table](#outputs) in the Outputs section for all modes. 6. Search all flows for `io.kestra.plugin.core.flow.ForEachItem` and migrate to `Loop` with a URI value as described above. 7. Validate each updated flow by saving it in the Kestra UI or via the API and confirming no parse errors. diff --git a/src/contents/docs/14.best-practices/11.foreach-and-foreachitem/index.md b/src/contents/docs/14.best-practices/11.foreach-and-foreachitem/index.md deleted file mode 100644 index df1bab59bad..00000000000 --- a/src/contents/docs/14.best-practices/11.foreach-and-foreachitem/index.md +++ /dev/null @@ -1,284 +0,0 @@ ---- -title: "ForEach vs ForEachItem in Kestra: When to Use Each" -h1: "ForEach vs ForEachItem: Scaling and Output Access" -sidebarTitle: ForEach vs ForEachItem -icon: /src/contents/docs/icons/best-practices.svg -description: Learn when to use ForEach or ForEachItem in Kestra, how they scale differently, and how to access their outputs correctly in downstream tasks. ---- - -Use `ForEach` and `ForEachItem` for different scaling and orchestration patterns. - -## Choose the right loop primitive - -Both tasks iterate over multiple items, but they do it in different ways: - -- `ForEach` creates child task runs inside the same execution. -- `ForEachItem` creates one subflow execution per batch of items. - -That design difference affects performance, restart behavior, and how you access outputs. - -## Decision guide - -Use `ForEach` when: - -- You already have a small list in memory, such as an input, a small JSON array, or a small fetched result. -- The work for each item is lightweight. -- You want to share outputs between sibling tasks inside the loop. -- You want a simple loop without introducing a subflow. - -Use `ForEachItem` when: - -- You need to process a large dataset or file. -- You want to split data into batches and scale processing through subflows. -- You need better isolation, troubleshooting, and restart behavior for individual batches. -- The data already lives in Kestra internal storage, or can be written there first. - -:::alert{type="warning"} -`ForEach` can generate many task runs in a single execution. For large fan-out or nested loops, prefer `ForEachItem` or a `Subflow`-based design to avoid oversized execution contexts and slower orchestration. -::: - -:::alert{type="info"} -`ForEachItem` expects `items` to be a Kestra internal storage URI, for example `{{ outputs.extract.uri }}` or a `FILE` input. If your source data is a regular JSON array, Excel file, Parquet file, or another non line-oriented format, convert it first. -::: - -## `Subflow` vs `ForEachItem` - -`Subflow` and `ForEachItem` both create child executions, but they solve different orchestration problems. - -Use `Subflow` when: - -- You want to trigger one child flow once. -- You already know the exact inputs to pass to that child flow. -- You want execution isolation without batching or iteration. -- You are decomposing a large workflow into smaller reusable modules. - -Use `ForEachItem` when: - -- You want to start many child flow executions from one dataset or file. -- You need batching by `rows`, `partitions`, or `bytes`. -- You want to process file-backed items incrementally at scale. -- You want Kestra to merge outputs from multiple child executions. - -Rule of thumb: - -- `Subflow` is one child execution for one unit of work. -- `ForEachItem` is many child executions for many units of work. - -For example, if you need to process one uploaded file in a dedicated child flow, use `Subflow`. If you need to split that file into many batches and process each batch in its own child flow execution, use `ForEachItem`. - -## Understand the main difference - -`ForEach` iterates over a list of values and exposes: - -- `{{ taskrun.value }}` for the current value -- `{{ taskrun.iteration }}` for the zero-based loop index - -`ForEachItem` iterates over batches of file-backed items and exposes: - -- `{{ taskrun.items }}` for the current batch file URI -- `{{ taskrun.iteration }}` for the zero-based batch index - -In practice: - -- `ForEach` is best when the iteration value itself is the thing you want to work with. -- `ForEachItem` is best when each iteration should receive a file or batch and hand it off to a subflow. - -## Best practices for `ForEach` - -- Keep the `values` list small to moderate in size. -- Use `concurrencyLimit` deliberately rather than leaving fan-out unbounded. -- If each iteration needs multiple tasks in parallel, put a `Parallel` task inside the loop instead of expecting child tasks to run concurrently by default. -- If iterating over JSON objects, remember that `taskrun.value` is a JSON string. Use `fromJson(taskrun.value)` to access properties. -- When referencing outputs from sibling tasks inside the same loop iteration, use `outputs.task_id[taskrun.value]`. - -### Example: use sibling outputs correctly inside `ForEach` - -```yaml -id: foreach_outputs -namespace: company.team - -tasks: - - id: enrich_regions - type: io.kestra.plugin.core.flow.ForEach - values: ["north", "south", "west"] - concurrencyLimit: 2 - tasks: - - id: metadata - type: io.kestra.plugin.core.output.OutputValues - values: - region: "{{ taskrun.value }}" - bucket: "landing-{{ taskrun.value }}" - - - id: build_message - type: io.kestra.plugin.core.debug.Return - format: "Load {{ outputs.metadata[taskrun.value].values.region }} into {{ outputs.metadata[taskrun.value].values.bucket }}" - - - id: log_one_result - type: io.kestra.plugin.core.log.Log - message: "{{ outputs.build_message['north'].value }}" -``` - -Why this pattern works: - -- Inside the loop, `outputs.metadata[taskrun.value]` reads the output from the current iteration. -- Outside the loop, `outputs.build_message['north'].value` reads the output for one specific loop value. - -### Example: iterate over JSON objects safely - -```yaml -id: foreach_json -namespace: company.team - -tasks: - - id: process_users - type: io.kestra.plugin.core.flow.ForEach - values: - - {"id": 101, "email": "a@example.com"} - - {"id": 102, "email": "b@example.com"} - tasks: - - id: log_user - type: io.kestra.plugin.core.log.Log - message: "User {{ fromJson(taskrun.value).id }} -> {{ fromJson(taskrun.value).email }}" -``` - -## Best practices for `ForEachItem` - -- Store the dataset in internal storage first and pass its URI to `items`. -- If your source file is CSV, JSON, Excel, or another external format, convert it to ION before passing it to `ForEachItem`. -- Batch by `rows`, `partitions`, or `bytes` based on how the downstream subflow processes data. -- Design the subflow so it can be rerun independently for one batch. -- Prefer passing `taskrun.items` to a `FILE` input in the subflow. -- If the parent flow must depend on child results, keep `wait: true`. -- If a child failure should fail the parent task, keep `transmitFailed: true`. - -### Example: process a file in batches with `ForEachItem` - -This pattern is recommended when each batch should run in its own execution. - -```yaml -id: parent_foreachitem -namespace: company.team - -tasks: - - id: download_orders_csv - type: io.kestra.plugin.core.http.Download - uri: https://huggingface.co/datasets/kestra/datasets/raw/main/csv/orders.csv - - - id: orders_to_ion - type: io.kestra.plugin.serdes.csv.CsvToIon - from: "{{ outputs.download_orders_csv.uri }}" - - - id: process_batches - type: io.kestra.plugin.core.flow.ForEachItem - items: "{{ outputs.orders_to_ion.uri }}" - batch: - rows: 2 - namespace: company.team - flowId: process_order_batch - wait: true - transmitFailed: true - inputs: - orders_file: "{{ taskrun.items }}" - - - id: log_merged_outputs_uri - type: io.kestra.plugin.core.log.Log - message: "{{ outputs.process_batches_merge.subflowOutputs }}" - - - id: preview_merged_outputs - type: io.kestra.plugin.core.log.Log - message: "{{ read(outputs.process_batches_merge.subflowOutputs) }}" -``` - -And the subflow: - -```yaml -id: process_order_batch -namespace: company.team - -inputs: - - id: orders_file - type: FILE - -tasks: - - id: inspect_batch - type: io.kestra.plugin.core.log.Log - message: "{{ read(inputs.orders_file) }}" - -outputs: - - id: batch_summary - type: STRING - value: "{{ 'Processed batch content: ' ~ read(inputs.orders_file) }}" -``` - -Here, `orders_file` is a batch file generated from the ION output of `CsvToIon`. Each subflow execution receives one batch file through `{{ taskrun.items }}`. - -## Use `ForEachItem` outputs correctly - -`ForEachItem` is best consumed through its internal helper task outputs: - -- `{{ outputs.task_id_split.splits }}` contains the file listing generated batch URIs. -- `{{ outputs.task_id_merge.subflowOutputs }}` contains a file with the merged outputs from the child subflows. - -If your `ForEachItem` task id is `process_batches`, those become: - -- `{{ outputs.process_batches_split.splits }}` -- `{{ outputs.process_batches_merge.subflowOutputs }}` - -This is different from `ForEach`, where you typically access outputs by loop value, such as `outputs.inner['north'].value`. - -### Example: consume merged subflow outputs - -If the subflow defines typed flow outputs, `ForEachItem` merges them into a file exposed by the internal merge task. In the example above, each child execution returns a `batch_summary` string, and the merge task gathers those subflow outputs into a single file. - -```yaml -id: parent_read_merged_outputs -namespace: company.team - -tasks: - - id: download_orders_csv - type: io.kestra.plugin.core.http.Download - uri: https://huggingface.co/datasets/kestra/datasets/raw/main/csv/orders.csv - - - id: orders_to_ion - type: io.kestra.plugin.serdes.csv.CsvToIon - from: "{{ outputs.download_orders_csv.uri }}" - - - id: process_batches - type: io.kestra.plugin.core.flow.ForEachItem - items: "{{ outputs.orders_to_ion.uri }}" - batch: - rows: 2 - namespace: company.team - flowId: process_order_batch - wait: true - transmitFailed: true - inputs: - orders_file: "{{ taskrun.items }}" - - - id: log_merged_outputs_uri - type: io.kestra.plugin.core.log.Log - message: "{{ outputs.process_batches_merge.subflowOutputs }}" - - - id: preview_merged_outputs - type: io.kestra.plugin.core.log.Log - message: "{{ read(outputs.process_batches_merge.subflowOutputs) }}" -``` - -Use `{{ outputs.process_batches_merge.subflowOutputs }}` when a downstream task needs the collected outputs from all child subflows. -If you want to inspect the merged file content directly, use `read(outputs.process_batches_merge.subflowOutputs)`. - -## Common mistakes to avoid - -- Do not use `ForEach` for very large datasets just because the input started as a JSON array. -- Do not pass a non-storage path or raw inline content to `ForEachItem.items`; it must be a Kestra internal storage URI. -- Do not assume sibling task outputs in `ForEach` use the plain `outputs.task_id.value` syntax; inside the loop, use `outputs.task_id[taskrun.value]`. -- Do not expect `ForEach` child tasks to run in parallel unless you either set loop concurrency or add a `Parallel` task inside the loop. -- Do not forget that `taskrun.iteration` starts at `0` for both `ForEach` and `ForEachItem`. - -## Recommended rule of thumb - -Use `ForEach` for orchestration over a relatively small list of values. - -Use `ForEachItem` for data processing over file-backed items or batches, especially when you need scale, restartability, or subflow isolation. - -For API details, see the [ForEach plugin documentation](/plugins/core/flow/io.kestra.plugin.core.flow.foreach), the [ForEachItem plugin documentation](/plugins/core/flow/io.kestra.plugin.core.flow.foreachitem), and the [Outputs documentation](../../05.workflow-components/06.outputs/index.md). diff --git a/src/contents/docs/14.best-practices/11.loop/index.md b/src/contents/docs/14.best-practices/11.loop/index.md new file mode 100644 index 00000000000..f31be09ec3c --- /dev/null +++ b/src/contents/docs/14.best-practices/11.loop/index.md @@ -0,0 +1,230 @@ +--- +title: "Loop Task Best Practices in Kestra" +h1: "Best Practices for the Loop Task" +sidebarTitle: Loop +icon: /src/contents/docs/icons/best-practices.svg +description: Best practices for using the Loop task in Kestra — output collection, concurrency, error handling, large-file processing, and subflow isolation patterns. +--- + +Use `Loop` for all iteration needs in Kestra. + +## Choose the right iteration pattern + +`Loop` runs child tasks for each item in a list, map, file, or URI list. Every iteration is an isolated sub-execution. + +Use a plain `Loop` when: + +- You need to run the same tasks for each item in a list or dataset. +- Each iteration should process one value, one object, or one file chunk. +- You want parallel iteration with controlled concurrency. +- You want per-iteration failure handling without stopping the entire loop. + +Use `Loop` + `Subflow` when: + +- You need full execution isolation per batch — own retries, own logs, own failure state. +- Each batch should be independently restartable. + +## Access the iteration value + +Inside a Loop, use `item.value` for the current value and `item.index` for the zero-based position. These are available in every child task, including those nested inside `If`, `Parallel`, or other flowable tasks — no parent traversal needed. + +```yaml +tasks: + - id: loop + type: io.kestra.plugin.core.flow.Loop + values: ["north", "south", "west"] + tasks: + - id: log + type: io.kestra.plugin.core.log.Log + message: "region={{ item.value }} index={{ item.index }}" +``` + +When iterating over JSON objects, `item.value` is a JSON string. Use `fromJson(item.value).field` to access properties — `item.value.field` does not work. + +```yaml +tasks: + - id: loop + type: io.kestra.plugin.core.flow.Loop + values: + - {"id": 101, "email": "a@example.com"} + - {"id": 102, "email": "b@example.com"} + tasks: + - id: log + type: io.kestra.plugin.core.log.Log + message: "User {{ fromJson(item.value).id }} -> {{ fromJson(item.value).email }}" +``` + +## Expose outputs explicitly + +Task outputs inside a loop are not visible outside it by default. Declare an `outputs:` block on the Loop task to surface values. Choose `fetchType` based on data volume: + +- `AUTO` — default, switches automatically based on whether `values` is a URI +- `FETCH` — collects all iteration results inline (suitable for small iteration counts) +- `STORE` — writes results to internal storage and exposes a URI (preferred for large iteration counts) + +After the loop: +- `outputs..outputs` is a list of per-iteration results +- `outputs..outputs[n].outputs.` accesses a specific iteration by index +- `loopOutputs(outputs..outputs, '')` extracts one field across all iterations as a flat list + +### Example: collect outputs and read them downstream + +```yaml +id: loop_outputs +namespace: company.team + +tasks: + - id: enrich_regions + type: io.kestra.plugin.core.flow.Loop + values: ["north", "south", "west"] + concurrencyLimit: 2 + fetchType: AUTO + outputs: + - id: bucket + type: STRING + value: "landing-{{ item.value }}" + - id: message + type: STRING + value: "{{ outputs.build_message.value }}" + tasks: + - id: build_message + type: io.kestra.plugin.core.debug.Return + format: "Load {{ item.value }} into landing-{{ item.value }}" + + - id: log_first + type: io.kestra.plugin.core.log.Log + message: "{{ outputs.enrich_regions.outputs[0].outputs.message }}" + + - id: log_all + type: io.kestra.plugin.core.log.Log + message: "{{ loopOutputs(outputs.enrich_regions.outputs, 'message') }}" +``` + +Inside the loop, sibling task outputs are accessed with plain `outputs.task_id.attribute` syntax — each iteration runs in its own isolated context, so there is no ambiguity. + +## Use `concurrencyLimit` deliberately + +- `1` (default) — sequential execution +- A positive integer — bounded parallelism; prefer this for heavy workloads +- `0` — unlimited; all iterations run simultaneously; avoid for large datasets without understanding resource implications + +## Process large files with Split and Loop + +For file-backed datasets, use `Split` to break the file into chunk URIs, then loop over the URI list. Each `item.value` is one chunk URI. + +Passing `values: "{{ outputs.split.uris }}"` where `outputs.split.uris` is a **list** is different from passing a single file URI string. A list iterates over elements; a single URI string iterates line-by-line through that file. + +```yaml +tasks: + - id: split + type: io.kestra.plugin.core.storage.Split + from: "{{ inputs.file }}" + rows: 100 + + - id: per_chunk + type: io.kestra.plugin.core.flow.Loop + values: "{{ outputs.split.uris }}" + concurrencyLimit: 4 + fetchType: FETCH + outputs: + - id: result_uri + type: STRING + value: "{{ outputs.process.value }}" + tasks: + - id: process + type: io.kestra.plugin.core.debug.Return + format: "processed chunk {{ item.index }}: {{ item.value }}" + + - id: summary + type: io.kestra.plugin.core.log.Log + message: "{{ loopOutputs(outputs.per_chunk.outputs, 'result_uri') }}" +``` + +## Handle per-iteration failures + +Set `transmitFailed: false` to continue the loop when individual iterations fail. Use `errors:` to run tasks per failed iteration, and `finally:` for a one-time cleanup block after all iterations finish. `errors:` requires `transmitFailed: false`; `finally:` always runs regardless. + +```yaml +tasks: + - id: loop + type: io.kestra.plugin.core.flow.Loop + values: ["ok", "boom", "ok"] + transmitFailed: false + tasks: + - id: maybe_fail + type: io.kestra.plugin.scripts.shell.Commands + commands: + - | + if [ "{{ item.value }}" = "boom" ]; then exit 1; fi + echo "ok {{ item.value }}" + errors: + - id: handle_error + type: io.kestra.plugin.core.log.Log + message: "Iteration {{ item.index }} ({{ item.value }}) failed" + finally: + - id: cleanup + type: io.kestra.plugin.core.log.Log + message: "Loop finished (with or without failures)" +``` + +## Use Loop + Subflow for isolated per-batch execution + +When each batch needs its own execution — independent retries, logs, and failure ownership — pair `Loop` with `Subflow`. The parent splits the data and fans out; the child flow receives one chunk URI per invocation and returns its result as a flow-level output. + +```yaml +# Parent flow +tasks: + - id: split + type: io.kestra.plugin.core.storage.Split + from: "{{ inputs.file }}" + rows: 100 + + - id: per_batch + type: io.kestra.plugin.core.flow.Loop + values: "{{ outputs.split.uris }}" + concurrencyLimit: 4 + fetchType: FETCH + outputs: + - id: result_uri + type: STRING + value: "{{ outputs.run_child.outputs.uri }}" + tasks: + - id: run_child + type: io.kestra.plugin.core.flow.Subflow + namespace: company.team + flowId: process_batch + wait: true + transmitFailed: true + inputs: + batch_uri: "{{ item.value }}" + + - id: concat + type: io.kestra.plugin.core.storage.Concat + files: "{{ loopOutputs(outputs.per_batch.outputs, 'result_uri') }}" + extension: .ion +``` + +## Compose Loop with supporting tasks + +`Loop` iterates. These tasks handle the rest — batching, transforming, stitching, and reducing. Each does one thing well; combine them around `Loop` to build larger pipelines. + +| Task | Role | When to reach for it | +|---|---|---| +| `io.kestra.plugin.core.storage.Split` | Batching | Split a single file into chunk URIs by `rows`, `bytes`, `partitions`, or `separator`. Feeds `Loop.values` for map-reduce. | +| `io.kestra.plugin.core.storage.Concat` | Stitching | Concatenate per-iteration output files into one before a reduce step. | +| `io.kestra.plugin.transform.Aggregate` | Reduce | Group records by one or more keys with `count()`, `sum()`, `max()`, and more. The reduce side of map-reduce. | +| `io.kestra.plugin.transform.Filter` | Predicate | Keep only rows where a boolean expression holds. | +| `io.kestra.plugin.transform.Map` | Project | Per-record rename, drop, or compute fields — SQL `SELECT`-style. | +| `io.kestra.plugin.transform.Unnest` | Explode | Flatten an array field into one row per element, carrying sibling fields through. | +| `io.kestra.plugin.core.flow.Subflow` | Isolate | Spawn a separate execution per iteration — own retries, own logs, own failure state. The `ForEachItem` replacement. | +| `io.kestra.plugin.core.flow.Parallel` | Fan-out | Run independent task groups concurrently inside a single iteration. | + +## Common mistakes to avoid + +- Do not use `taskrun.value` or `taskrun.iteration` — use `item.value` and `item.index`. +- Do not access `item.value.field` directly on object values — use `fromJson(item.value).field`. +- Do not expect loop outputs to be visible downstream without declaring an `outputs:` block. +- Do not use `outputs.task_id[item.value]` inside a loop — sibling outputs are accessed with plain `outputs.task_id.attribute`. +- Do not set `concurrencyLimit: 0` on very large datasets without considering memory and worker capacity. + +For more details, see the [Loop task documentation](/plugins/core/flow/io.kestra.plugin.core.flow.loop) and the [Flowable Tasks reference](../../05.workflow-components/01.tasks/00.flowable-tasks/index.md#loop). diff --git a/src/contents/docs/15.how-to-guides/loop/index.md b/src/contents/docs/15.how-to-guides/loop/index.md index 48a15d28366..5269a683fab 100644 --- a/src/contents/docs/15.how-to-guides/loop/index.md +++ b/src/contents/docs/15.how-to-guides/loop/index.md @@ -1,16 +1,16 @@ --- title: Loop Over a List of Values -h1: Iterate Over Lists with the ForEach Task +h1: Iterate Over Lists with the Loop Task icon: /src/contents/docs/icons/tutorial.svg stage: Intermediate topics: - Kestra Workflow Components -description: Learn how to iterate over lists of values in Kestra workflows using the ForEach task to execute tasks for each item efficiently. +description: Learn how to iterate over a list of values in Kestra workflows using the Loop task, access iteration context, collect outputs, and run iterations in parallel. --- How to iterate over a list of values in your flow. -In this guide, you will learn how to iterate over a list of values using the `ForEach` task. This task enables you to loop through a list of values and execute specific tasks for each value in the list. This approach is useful for scenarios where multiple similar tasks need to be run for different inputs. +In this guide, you will learn how to use the `Loop` task to iterate over a list of values and run tasks for each item. Each iteration runs as an isolated sub-execution with access to the current value via `item.value` and the zero-based index via `item.index`. ## Prerequisites @@ -19,70 +19,109 @@ Before you begin: - Deploy [Kestra](../../02.installation/index.mdx) in your preferred development environment. - Ensure you have a [basic understanding of how to run Kestra flows.](../../03.tutorial/index.mdx) -## Loop over nested lists of values +## Basic iteration -This example demonstrates how to use `ForEach` to loop over a list of strings and then loop through a nested list for each string. +The simplest use of `Loop` iterates over a static list and runs child tasks for each item. The example below makes an API call for each author in the list. -You can access the current iteration value using the variable `{{ taskrun.value }}` or `{{ parent.taskrun.value }}` if you are in a nested child task. Additionally, you can access the batch or iteration number with `{{ taskrun.iteration }}`. +```yaml +id: loop_basic +namespace: company.team + +tasks: + - id: loop + type: io.kestra.plugin.core.flow.Loop + values: ["pynchon", "dostoyevsky", "hedayat"] + tasks: + - id: api + type: io.kestra.plugin.core.http.Request + uri: "https://openlibrary.org/search.json?author={{ item.value }}&sort=new" +``` + +Inside each iteration: +- `{{ item.value }}` — the current value from the list +- `{{ item.index }}` — the zero-based position (0, 1, 2, …) -To see the flow in action, define the `each_nested` flow as shown below: +After execution, the Gantt view shows a separate task group for each author. + +When `values` contains objects, each `item.value` is a JSON string. Use `fromJson(item.value).field` to access fields — `item.value.field` does not work. + +## Nested loops + +To iterate over multiple dimensions, nest `Loop` tasks. The inner loop accesses the outer loop's value with `{{ item.parent.value }}`. For three or more levels, `{{ item.parents[1].value }}` is the grandparent — `item.parents[0]` is the same as `item.parent`. ```yaml -id: each_nested +id: loop_nested namespace: company.team tasks: - - id: 1_each - type: io.kestra.plugin.core.flow.ForEach - values: '["s1", "s2", "s3"]' + - id: outer + type: io.kestra.plugin.core.flow.Loop + values: ["bucket1", "bucket2"] tasks: - - id: 1-1_return - type: io.kestra.plugin.core.debug.Return - format: "{{task.id}} > {{taskrun.value}} > {{taskrun.startDate}}" - - id: 1-2_each - type: io.kestra.plugin.core.flow.ForEach - values: '["a a", "b b"]' + - id: inner + type: io.kestra.plugin.core.flow.Loop + values: [2025, 2026] tasks: - - id: 1-2-1_return - type: io.kestra.plugin.core.debug.Return - format: "{{task.id}} > {{taskrun.value}} > {{taskrun.startDate}}" - - id: 1-2-2_return - type: io.kestra.plugin.core.debug.Return - format: "{{task.id}} > {{ outputs['1-2-1_return'].s1[taskrun.value].value }} >> get {{ outputs['1-2-1_return']['s1'][taskrun.value].value }} > {{taskrun.startDate}}" - - id: 1-3_return - type: io.kestra.plugin.core.debug.Return - format: "{{task.id}} > {{ outputs['1-1_return'][taskrun.value].value }} > {{taskrun.startDate}}" - - id: 2_return - type: io.kestra.plugin.core.debug.Return - format: "{{task.id}} > {{outputs['1-2-1_return'].s1['a a'].value}}" + - id: log + type: io.kestra.plugin.core.log.Log + message: "bucket={{ item.parent.value }} year={{ item.value }}" ``` -Save and execute the `each_nested` flow. - -The above flow, when executed, iterates over a nested list of values, logging messages at each level of iteration to track the processing of both the outer and inner list items. - -Within the flow: +## Collect outputs across iterations -- `1_each`: Uses the `ForEach` task to iterate over the list `["s1", "s2", "s3"]`. For each value, it runs the nested tasks defined within. +By default, outputs produced inside a loop are not visible to tasks that run after it. Declare an `outputs:` block on the Loop task to surface values explicitly. After the loop, `outputs.loop.outputs` is a list of per-iteration results. Use `loopOutputs()` to extract one field across all iterations as a flat list. - - `1-1_return`: Logs the task ID, the current list value, and the task run start time. +```yaml +id: loop_outputs +namespace: company.team - - `1-2_each`: Iterates over a second list `["a a", "b b"]` and runs a set of tasks for each value in this nested list. +tasks: + - id: loop + type: io.kestra.plugin.core.flow.Loop + values: ["alpha", "beta", "gamma"] + fetchType: AUTO + outputs: + - id: label + type: STRING + value: "{{ outputs.process.value }}" + tasks: + - id: process + type: io.kestra.plugin.core.debug.Return + format: "processed {{ item.value }}" - - `1-2-1_return`: Logs the task ID, the nested list value, and the start time of the task run. + - id: read_outputs + type: io.kestra.plugin.core.log.Log + message: "All results: {{ loopOutputs(outputs.loop.outputs, 'label') }}" +``` - - `1-2-2_return`: Logs a custom output from `1-2-1_return`, which shows how to access outputs from previous iterations within the nested loop. +## Run iterations in parallel - - `1-3_return`: Logs the output from `1-1_return` after the inner loop is completed and displays the corresponding value processed in the outer loop. +Set `concurrencyLimit` to `0` to run all iterations simultaneously, or to a positive integer to cap how many run at once. Combine with an inner `Parallel` task to also parallelise work within each iteration. -- `2_return`: Fetches the output from the nested loop (`1-2-1_return` for the value `a a`) and logs it. +```yaml +id: loop_parallel +namespace: company.team +tasks: + - id: loop + type: io.kestra.plugin.core.flow.Loop + values: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + concurrencyLimit: 0 + tasks: + - id: parallel + type: io.kestra.plugin.core.flow.Parallel + tasks: + - id: log + type: io.kestra.plugin.core.log.Log + message: "Processing {{ item.value }}" + - id: shell + type: io.kestra.plugin.scripts.shell.Commands + commands: + - "echo done {{ item.value }}" +``` ## Next steps -Now that you've seen how to loop over a list of values using `ForEach`, you can apply this technique to any scenario where multiple iterations of similar tasks are needed. You can further extend this flow by: -- Adding more complex nested loops. -- Using dynamic input values instead of hardcoded lists. -- Logging or processing additional data from each iteration. - -For more advanced use cases, refer to Kestra’s official [ForEach](/plugins/core/flow/io.kestra.plugin.core.flow.foreach) task documentation and the [Best Practices for ForEach and ForEachItem](../../14.best-practices/11.foreach-and-foreachitem/index.md) guide, which covers how to access sibling task outputs inside and outside the loop, when to use `ForEachItem` instead, and common mistakes to avoid. +- For the full Loop property reference, see the [Loop task documentation](/plugins/core/flow/io.kestra.plugin.core.flow.loop). +- For output collection patterns, error handling, and map-reduce examples, see the [Flowable Tasks](../../05.workflow-components/01.tasks/00.flowable-tasks/index.md#loop) reference. +- For Loop best practices, see the [Loop best practices guide](../../14.best-practices/11.loop/index.md). diff --git a/src/contents/docs/expressions/01.context/index.mdx b/src/contents/docs/expressions/01.context/index.mdx index 9a941bc3853..f6b737b8f15 100644 --- a/src/contents/docs/expressions/01.context/index.mdx +++ b/src/contents/docs/expressions/01.context/index.mdx @@ -55,8 +55,6 @@ The Debug Expression console is available in the Kestra UI under **Executions | `{{ taskrun.startDate }}` | Start date of the current task run | | `{{ taskrun.attemptsCount }}` | Retry and restart attempt count | | `{{ taskrun.parentId }}` | Parent task run identifier for nested tasks | -| `{{ taskrun.value }}` | Current loop or flowable value | -| `{{ parent.taskrun.value }}` | Value of the nearest parent task run | | `{{ parent.outputs }}` | Outputs of the nearest parent task run | | `{{ parents }}` | List of parent task runs | | `{{ labels }}` | Execution labels accessible by key | diff --git a/src/contents/redirects/docs.yml b/src/contents/redirects/docs.yml index edb7cdd57d1..708e52bda08 100644 --- a/src/contents/redirects/docs.yml +++ b/src/contents/redirects/docs.yml @@ -22,6 +22,8 @@ to: "/docs/api-reference" - regexp: "/docs/best-practices/pebble-templating-with-namespace-files/?$" to: "/docs/best-practices/expressions-with-namespace-files" +- regexp: "^/docs/best-practices/foreach-and-foreachitem(/.*)?$" + to: "/docs/best-practices/loop" - regexp: "/docs/concepts/editor(/.*)?$" to: "/docs/ui/flows" - regexp: "/docs/concepts/expression/basic-usage" From 22535c988ee5bb37909d778f541b70cdc32c4fed Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Tue, 19 May 2026 10:18:45 +0200 Subject: [PATCH 42/52] docs(read-only-secrets): add exclude on tags Part of https://github.com/kestra-io/kestra-ee/issues/6939 --- .../02.governance/read-only-secrets/index.md | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/contents/docs/07.enterprise/02.governance/read-only-secrets/index.md b/src/contents/docs/07.enterprise/02.governance/read-only-secrets/index.md index 2b725734d7d..1ebbb84a3bb 100644 --- a/src/contents/docs/07.enterprise/02.governance/read-only-secrets/index.md +++ b/src/contents/docs/07.enterprise/02.governance/read-only-secrets/index.md @@ -241,6 +241,48 @@ kestra: application: kestra-production ``` +## Exclude secrets by tags + +Use `excluded-tags` to hide secrets from Kestra based on their tags. Any secret whose tags match at least one key-value pair in `excluded-tags` is excluded from Kestra's view, even if it would otherwise be included by `filter-on-tags`. This filter applies only when `read-only: true` is set and is supported for AWS Secrets Manager, Azure Key Vault, and Google Secret Manager. + +When both `filter-on-tags` and `excluded-tags` are configured, a secret must match all entries in `filter-on-tags.tags` and must not match any entry in `excluded-tags`. + +The following examples exclude secrets tagged `hidden: "true"` for each supported provider: + +```yaml +kestra: + secret: + type: aws-secret-manager + read-only: true + aws-secret-manager: + excluded-tags: + hidden: "true" +``` + +```yaml +kestra: + secret: + type: azure-key-vault + read-only: true + azure-key-vault: + excluded-tags: + hidden: "true" +``` + +```yaml +kestra: + secret: + type: google-secret-manager + read-only: true + google-secret-manager: + excluded-tags: + hidden: "true" +``` + +:::alert{type="info"} +AWS Secrets Manager does not support negative tag filtering in its `ListSecrets` API. Kestra evaluates `excluded-tags` client-side after fetching the secret list from AWS. +::: + ## Filter secrets by prefix For AWS Secrets Manager, you can also filter secrets by a name prefix when using read-only mode. Use `filter-on-prefix.prefix` to select secrets whose names start with the given prefix and `filter-on-prefix.keep-prefix` to control whether the prefix is kept in the Kestra secret key. From ddd940c860fd9e334a9e2e63f140a4858373d1a1 Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Tue, 19 May 2026 15:56:26 +0200 Subject: [PATCH 43/52] docs(functions): remove parentOutput part of https://github.com/kestra-io/kestra-ee/issues/7863 --- .../expressions/04.functions/04.workflow/index.mdx | 11 +---------- src/contents/docs/expressions/04.functions/index.mdx | 2 +- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/contents/docs/expressions/04.functions/04.workflow/index.mdx b/src/contents/docs/expressions/04.functions/04.workflow/index.mdx index e63ca802ca6..fd1f40bbc78 100644 --- a/src/contents/docs/expressions/04.functions/04.workflow/index.mdx +++ b/src/contents/docs/expressions/04.functions/04.workflow/index.mdx @@ -1,7 +1,7 @@ --- title: "Workflow Helper Functions in Kestra Expressions" h1: "Workflow Helper Functions" -description: Reference for Kestra's workflow and execution helper functions — loopOutputs(), errorLogs(), currentEachOutput(), tasksWithState(), iterationOutput(), parentOutput(), and appLink(). +description: Reference for Kestra's workflow and execution helper functions — loopOutputs(), errorLogs(), currentEachOutput(), tasksWithState(), iterationOutput(), and appLink(). sidebarTitle: Workflow Functions icon: /src/contents/docs/icons/expression.svg --- @@ -69,15 +69,6 @@ Retrieves the output of a specific iteration from a previous task. Both argument {{ iterationOutput(outputs.myTask, 2).value }} ``` -## `parentOutput()` - -Retrieves the output of a parent task. The optional `index` argument specifies which ancestor to target; omitting it returns the direct parent's output: - -```twig -{{ parentOutput() }} -{{ parentOutput(1) }} -``` - ## `appLink()` Enterprise Edition's `appLink()` builds links back to Kestra Apps: diff --git a/src/contents/docs/expressions/04.functions/index.mdx b/src/contents/docs/expressions/04.functions/index.mdx index 825844ff74b..6f6a8272a27 100644 --- a/src/contents/docs/expressions/04.functions/index.mdx +++ b/src/contents/docs/expressions/04.functions/index.mdx @@ -17,7 +17,7 @@ Functions are best thought of as helpers that either fetch something, compute so - [Rendering and debugging](./01.rendering/index.mdx) — `render()`, `renderOnce()`, `printContext()`, template inheritance helpers - [Data access](./02.data-access/index.mdx) — `secret()`, `credential()`, `read()`, `fileURI()`, `kv()`, `encrypt()`, `decrypt()` - [Data parsing](./03.parsing/index.mdx) — `fromJson()`, `fromIon()`, `yaml()` -- [Workflow helpers](./04.workflow/index.mdx) — `errorLogs()`, `currentEachOutput()`, `tasksWithState()`, `iterationOutput()`, `parentOutput()`, `appLink()` +- [Workflow helpers](./04.workflow/index.mdx) — `errorLogs()`, `currentEachOutput()`, `tasksWithState()`, `iterationOutput()`, `appLink()` - [Utilities](./05.utilities/index.mdx) — `now()`, `uuid()`, `randomInt()`, `http()`, `fileSize()`, `fileExists()`, and more - [Date and calendar](./06.dates/index.mdx) — `isWeekend()`, `isPublicHoliday()`, `dayOfWeek()`, `monthOfYear()`, and more From 7aa532239b5486df02f02913369e59c606471b7e Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Tue, 19 May 2026 16:04:29 +0200 Subject: [PATCH 44/52] docs(expressions): switch to fromJson() Part of https://github.com/kestra-io/kestra-ee/issues/7501 --- .../docs/03.tutorial/05.flowable/index.md | 6 ++-- .../docs/03.tutorial/06.errors/index.md | 8 +++--- .../06.concepts/02.namespace-files/index.md | 2 +- .../docs/06.concepts/05.kv-store/index.md | 16 +++++------ .../v2.0.0/json-function-removed/index.md | 28 +++++++++++++++++++ 5 files changed, 44 insertions(+), 16 deletions(-) create mode 100644 src/contents/docs/11.migration-guide/v2.0.0/json-function-removed/index.md diff --git a/src/contents/docs/03.tutorial/05.flowable/index.md b/src/contents/docs/03.tutorial/05.flowable/index.md index fb2d937a99c..d0b33b4c7e6 100644 --- a/src/contents/docs/03.tutorial/05.flowable/index.md +++ b/src/contents/docs/03.tutorial/05.flowable/index.md @@ -20,7 +20,7 @@ For example, you can use the [If task](/plugins/core/flow/io.kestra.plugin.core. The example below redesigns the flow to use a `SELECT` input for product category rather than a `STRING` URI, while still calling [dummyjson](https://dummyjson.com). An API request is made based on the selected category — `beauty` or `notebooks` (one does not exist). -The `check_products` If task has a `condition` of `"{{ json(outputs.api.body).products | length > 0 }}"` (i.e., checking whether the API body is not empty and contains at least one product). The log message then depends on whether the actual product category exists or not. The `then` property defines the action for a true condition, and the `else` property defines the action for a false result. +The `check_products` If task has a `condition` of `"{{ fromJson(outputs.api.body).products | length > 0 }}"` (i.e., checking whether the API body is not empty and contains at least one product). The log message then depends on whether the actual product category exists or not. The `then` property defines the action for a true condition, and the `else` property defines the action for a false result. ```yaml id: getting_started @@ -41,11 +41,11 @@ tasks: - id: check_products type: io.kestra.plugin.core.flow.If - condition: "{{ json(outputs.api.body).products | length > 0 }}" + condition: "{{ fromJson(outputs.api.body).products | length > 0 }}" then: - id: log_status type: io.kestra.plugin.core.log.Log - message: "Found {{ json(outputs.api.body).products | length }} products for category {{ inputs.category }}" + message: "Found {{ fromJson(outputs.api.body).products | length }} products for category {{ inputs.category }}" - id: python type: io.kestra.plugin.scripts.python.Script containerImage: python:slim diff --git a/src/contents/docs/03.tutorial/06.errors/index.md b/src/contents/docs/03.tutorial/06.errors/index.md index 883e15277d1..3b6b913b1a7 100644 --- a/src/contents/docs/03.tutorial/06.errors/index.md +++ b/src/contents/docs/03.tutorial/06.errors/index.md @@ -71,11 +71,11 @@ tasks: - id: check_products type: io.kestra.plugin.core.flow.If - condition: "{{ json(outputs.api.body).products | length > 0 }}" + condition: "{{ fromJson(outputs.api.body).products | length > 0 }}" then: - id: log_status type: io.kestra.plugin.core.log.Log - message: "Found {{ json(outputs.api.body).products | length }} products for category {{ inputs.category }}" + message: "Found {{ fromJson(outputs.api.body).products | length }} products for category {{ inputs.category }}" - id: python type: io.kestra.plugin.scripts.python.Script containerImage: python:slim @@ -237,11 +237,11 @@ tasks: - id: check_products type: io.kestra.plugin.core.flow.If - condition: "{{ json(outputs.api.body).products | length > 0 }}" + condition: "{{ fromJson(outputs.api.body).products | length > 0 }}" then: - id: log_status type: io.kestra.plugin.core.log.Log - message: "Found {{ json(outputs.api.body).products | length }} products for category {{ inputs.category }}" + message: "Found {{ fromJson(outputs.api.body).products | length }} products for category {{ inputs.category }}" - id: python type: io.kestra.plugin.scripts.python.Script containerImage: python:slim diff --git a/src/contents/docs/06.concepts/02.namespace-files/index.md b/src/contents/docs/06.concepts/02.namespace-files/index.md index d559bb14326..1bad6f506a6 100644 --- a/src/contents/docs/06.concepts/02.namespace-files/index.md +++ b/src/contents/docs/06.concepts/02.namespace-files/index.md @@ -35,7 +35,7 @@ tasks: tasks: - id: return type: io.kestra.plugin.core.debug.Return - format: "{{ json(taskrun.value) }}" + format: "{{ fromJson(taskrun.value) }}" triggers: - id: query_trigger diff --git a/src/contents/docs/06.concepts/05.kv-store/index.md b/src/contents/docs/06.concepts/05.kv-store/index.md index f6abb8e085b..043f26994bf 100644 --- a/src/contents/docs/06.concepts/05.kv-store/index.md +++ b/src/contents/docs/06.concepts/05.kv-store/index.md @@ -132,7 +132,7 @@ tasks: values: my_key: "{{ kv('my_key') }}" simple_string: "{{ kv('simple_string') }}" - favorite_song: "{{ json(kv('json_kv')).song }}" + favorite_song: "{{ fromJson(kv('json_kv')).song }}" ``` You can use the `io.kestra.plugin.core.kv.Set` task to create or modify any KV pair. When modifying existing values, you can leverage the `overwrite` boolean parameter to control whether to overwrite the existing value or fail if a value for that key already exists. By default, the `overwrite` parameter is set to `true` so that the existing value is always updated. @@ -215,9 +215,9 @@ tasks: ### Read and parse JSON-type values from KV pairs -To parse JSON values in Kestra's templated expressions, make sure to wrap the `kv()` call in the `json()` function like the following: `"{{ json(kv('your_json_key')).json_property }}"`. +To parse JSON values in Kestra's templated expressions, wrap the `kv()` call in the `fromJson()` function: `"{{ fromJson(kv('your_json_key')).json_property }}"`. -The following example demonstrates how to parse values from JSON-type KV pairs in a flow: +This example sets a JSON KV pair and reads individual fields using `fromJson()`: ```yaml id: kv_json_flow namespace: company.team @@ -239,10 +239,10 @@ tasks: - id: parse_json_kv type: io.kestra.plugin.core.log.Log message: - - "Author: {{ json(kv('favorite_song')).author }}" - - "Song: {{ json(kv('favorite_song')).song }}" - - "Album name: {{ json(kv('favorite_song')).album.name }}" - - "Album release date: {{ json(kv('favorite_song')).album.release_date }}" + - "Author: {{ fromJson(kv('favorite_song')).author }}" + - "Song: {{ fromJson(kv('favorite_song')).song }}" + - "Album name: {{ fromJson(kv('favorite_song')).album.name }}" + - "Album release date: {{ fromJson(kv('favorite_song')).album.release_date }}" - id: get type: io.kestra.plugin.core.kv.Get @@ -250,7 +250,7 @@ tasks: - id: parse_json_from_kv type: io.kestra.plugin.core.log.Log - message: "Country: {{ json(outputs.get.value).album.name }}" + message: "Album name: {{ fromJson(outputs.get.value).album.name }}" ``` diff --git a/src/contents/docs/11.migration-guide/v2.0.0/json-function-removed/index.md b/src/contents/docs/11.migration-guide/v2.0.0/json-function-removed/index.md new file mode 100644 index 00000000000..1b7c2f66f86 --- /dev/null +++ b/src/contents/docs/11.migration-guide/v2.0.0/json-function-removed/index.md @@ -0,0 +1,28 @@ +--- +title: json() Function Removed +sidebarTitle: json() → fromJson() +icon: /src/contents/docs/icons/migration-guide.svg +release: 2.0.0 +editions: ["OSS", "EE"] +description: The json() Pebble function has been removed in Kestra 2.0.0. Replace all calls to json() with fromJson() — the signature is identical. +--- + +The `json()` Pebble function has been removed in Kestra 2.0.0. Replace every call to `json(...)` with `fromJson(...)`. The function signature and behavior are identical. + +## Before + +```twig +{{ json(outputs.request.body).products[0].id }} +{{ json(kv('my_json_key')).field }} +``` + +## After + +```twig +{{ fromJson(outputs.request.body).products[0].id }} +{{ fromJson(kv('my_json_key')).field }} +``` + +## What to update + +Search your flows and templates for `json(` and replace each occurrence with `fromJson(`. The `json` Pebble test (`{% if x is json %}`) is unrelated and still works — only the function call form changes. From 968d1d656c3d0539769fc3e919218d7dab2d179c Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Thu, 21 May 2026 14:09:51 +0200 Subject: [PATCH 45/52] docs(expressions): add isLastWorkingDay Part of https://github.com/kestra-io/kestra/issues/16008 --- .../docs/expressions/04.functions/06.dates/index.mdx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/contents/docs/expressions/04.functions/06.dates/index.mdx b/src/contents/docs/expressions/04.functions/06.dates/index.mdx index 96c3b19f477..cc19500c8a2 100644 --- a/src/contents/docs/expressions/04.functions/06.dates/index.mdx +++ b/src/contents/docs/expressions/04.functions/06.dates/index.mdx @@ -33,6 +33,17 @@ Returns `true` if the date is the Nth occurrence of the given weekday in its mon {{ isDayWeekInMonth(trigger.date, 'MONDAY', 'FIRST') }} ``` +## `isLastWorkingDay()` + +Returns `true` if the date is the last working day of its month. Working days default to Monday–Friday. An optional second argument overrides which days count as working days using a comma-separated list of uppercase day names: + +```twig +{{ isLastWorkingDay(trigger.date) }} +{{ isLastWorkingDay(trigger.date, 'MONDAY,TUESDAY,WEDNESDAY,THURSDAY') }} +``` + +The `date` argument accepts any ISO 8601 date or datetime string. Combine with `isPublicHoliday()` if you also need to exclude public holidays. + ## `dayOfWeek()` Returns the uppercase day name such as `MONDAY`: From d012c9214a01af67369a0c1ec98f5fa86e453751 Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Thu, 21 May 2026 14:45:38 +0200 Subject: [PATCH 46/52] docs(read-only-secrets): add tags filters Part of https://github.com/kestra-io/kestra-ee/issues/5953 & https://github.com/kestra-io/kestra-ee/issues/7890 --- .../02.governance/read-only-secrets/index.md | 134 +++++++++++++++++- 1 file changed, 127 insertions(+), 7 deletions(-) diff --git a/src/contents/docs/07.enterprise/02.governance/read-only-secrets/index.md b/src/contents/docs/07.enterprise/02.governance/read-only-secrets/index.md index 1ebbb84a3bb..43f13b84879 100644 --- a/src/contents/docs/07.enterprise/02.governance/read-only-secrets/index.md +++ b/src/contents/docs/07.enterprise/02.governance/read-only-secrets/index.md @@ -202,11 +202,11 @@ After saving the flow and executing, we can see that Kestra successfully accesse ## Filter secrets by tags -When integrating an external secrets manager in read-only mode, you can filter which secrets are visible in Kestra by matching [tags](../secrets-manager/index.md#default-tags). This is supported for AWS Secrets Manager, Azure Key Vault, and Google Secret Manager. +When integrating an external secrets manager in read-only mode, you can filter which secrets are visible in Kestra by matching [tags](../secrets-manager/index.md#default-tags). Set `read-only: true` and configure `filter-on-tags` with the key/value pairs to match. -- Set `read-only: true` and configure `filter-on-tags.tags` as a map of key/value pairs to match. - -Below are example configurations for AWS Secrets Manager, Azure Key Vault, and Google Secret Manager: +:::alert{type="info"} +AWS Secrets Manager, Azure Key Vault, and Google Secret Manager use a nested `tags` sub-key under `filter-on-tags`. All other providers accept `filter-on-tags` as a flat map of key/value pairs. +::: ```yaml kestra: @@ -241,11 +241,71 @@ kestra: application: kestra-production ``` +```yaml +kestra: + secret: + type: vault + read-only: true + vault: + filter-on-tags: + application: kestra-production +``` + +```yaml +kestra: + secret: + type: cyberark + read-only: true + cyberark: + filter-on-tags: + application: kestra-production +``` + +```yaml +kestra: + secret: + type: doppler + read-only: true + doppler: + filter-on-tags: + application: kestra-production +``` + +```yaml +kestra: + secret: + type: 1password + read-only: true + 1password: + filter-on-tags: + application: kestra-production +``` + +```yaml +kestra: + secret: + type: beyondtrust + read-only: true + beyondtrust: + filter-on-tags: + application: kestra-production +``` + +```yaml +kestra: + secret: + type: delinea + read-only: true + delinea: + filter-on-tags: + application: kestra-production +``` + ## Exclude secrets by tags -Use `excluded-tags` to hide secrets from Kestra based on their tags. Any secret whose tags match at least one key-value pair in `excluded-tags` is excluded from Kestra's view, even if it would otherwise be included by `filter-on-tags`. This filter applies only when `read-only: true` is set and is supported for AWS Secrets Manager, Azure Key Vault, and Google Secret Manager. +Use `excluded-tags` to hide secrets from Kestra based on their tags. Any secret whose tags match at least one key-value pair in `excluded-tags` is excluded from Kestra's view, even if it would otherwise be included by `filter-on-tags`. This filter applies only when `read-only: true` is set. -When both `filter-on-tags` and `excluded-tags` are configured, a secret must match all entries in `filter-on-tags.tags` and must not match any entry in `excluded-tags`. +When both `filter-on-tags` and `excluded-tags` are configured, a secret must match all entries in `filter-on-tags` and must not match any entry in `excluded-tags`. The following examples exclude secrets tagged `hidden: "true"` for each supported provider: @@ -279,8 +339,68 @@ kestra: hidden: "true" ``` +```yaml +kestra: + secret: + type: vault + read-only: true + vault: + excluded-tags: + hidden: "true" +``` + +```yaml +kestra: + secret: + type: cyberark + read-only: true + cyberark: + excluded-tags: + hidden: "true" +``` + +```yaml +kestra: + secret: + type: doppler + read-only: true + doppler: + excluded-tags: + hidden: "true" +``` + +```yaml +kestra: + secret: + type: 1password + read-only: true + 1password: + excluded-tags: + hidden: "true" +``` + +```yaml +kestra: + secret: + type: beyondtrust + read-only: true + beyondtrust: + excluded-tags: + hidden: "true" +``` + +```yaml +kestra: + secret: + type: delinea + read-only: true + delinea: + excluded-tags: + hidden: "true" +``` + :::alert{type="info"} -AWS Secrets Manager does not support negative tag filtering in its `ListSecrets` API. Kestra evaluates `excluded-tags` client-side after fetching the secret list from AWS. +AWS Secrets Manager does not support negative tag filtering in its `ListSecrets` API. Kestra evaluates `excluded-tags` client-side after fetching the secret list from AWS. For CyberArk, Doppler, 1Password, Vault, BeyondTrust, and Delinea, both `filter-on-tags` and `excluded-tags` are also evaluated client-side. ::: ## Filter secrets by prefix From bfb176e67f9c643426ed83cdde2d28be461ddd0a Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Fri, 22 May 2026 09:32:36 +0200 Subject: [PATCH 47/52] docs(plugin): correct exclusion behavior Part of https://github.com/kestra-io/kestra-ee/issues/6998 --- .../02.governance/allowed-plugins/index.md | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/contents/docs/07.enterprise/02.governance/allowed-plugins/index.md b/src/contents/docs/07.enterprise/02.governance/allowed-plugins/index.md index e552f569860..1a85202c581 100644 --- a/src/contents/docs/07.enterprise/02.governance/allowed-plugins/index.md +++ b/src/contents/docs/07.enterprise/02.governance/allowed-plugins/index.md @@ -12,9 +12,19 @@ How to configure Kestra to allow or restrict specific plugins. ## Allowed & Restricted Plugins -Kestra comes with the full library of official plugins by default. However, in some cases you may want to restrict which plugins are available to specific teams or users. For example, you might allow a team to use only BigQuery tasks while blocking script execution. Kestra enables this by letting you define allowlists (`includes`) and blocklists (`excludes`) using plugin names or regular expressions. +Kestra comes with the full library of official plugins by default. However, in some cases you may want to restrict which plugins are available to specific teams or users. For example, you might allow a team to use only BigQuery tasks while blocking script execution. Kestra enables this by letting you define allowlists (`includes`) and blocklists (`excludes`) in your [Plugins and Execution configuration](../../../configuration/04.plugins-and-execution/index.md). -To allow specific plugins, add the `includes` attribute in your [Plugins and Execution configuration](../../../configuration/04.plugins-and-execution/index.md) file and list the approved plugins or use a regular expression. Below is an example that `includes` all plugins from the `io.kestra` package using a regular expression. +## Matching syntax + +Each entry in `includes` or `excludes` supports three formats: + +- **Trailing wildcard** (`io.kestra.*`) — matches all plugins whose class name starts with the given prefix. The trailing `*` is stripped and the remainder is used as a prefix. +- **Regex** (`regex:`) — matches using a full Java regular expression. Example: `regex:^io\.kestra\.plugin\.core\.flow\.Parallel$`. +- **Plain value** (`io.kestra.plugin.core.flow.Parallel`) — prefix match for backward compatibility. Behaves the same as adding a trailing `*`. Use explicit trailing wildcard or `regex:` for clarity. + +## Allowed plugins + +To allow specific plugins, add the `includes` attribute and list the approved plugins. The following example allows all plugins from the `io.kestra` package: ```yaml kestra: @@ -26,7 +36,7 @@ kestra: ## Restricted plugins -To restrict certain plugins, add the `excludes` attribute in your [Plugins and Execution configuration](../../../configuration/04.plugins-and-execution/index.md) file and list the disallowed plugins or use a regular expression. Below is the previous example with `excludes` added to disallow the `io.kestra.plugin.core.debug.Echo` plugin. +To restrict certain plugins, add the `excludes` attribute. The following example allows all `io.kestra` plugins while blocking `io.kestra.plugin.core.debug.Echo`: ```yaml kestra: @@ -37,3 +47,13 @@ kestra: excludes: - io.kestra.plugin.core.debug.Echo ``` + +Use the `regex:` prefix for more precise pattern matching, such as excluding a single plugin without prefix side-effects: + +```yaml +kestra: + plugins: + security: + excludes: + - regex:^io\.kestra\.plugin\.core\.flow\.Parallel$ +``` From 59f869ce1d2456ac060ac63bee3d51631d770c2f Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Wed, 27 May 2026 15:02:54 +0200 Subject: [PATCH 48/52] docs(mcptrigger): add docs across set Part of https://github.com/kestra-io/kestra/issues/12593 & https://github.com/kestra-io/kestra/issues/15040 --- public/llms.txt | 6 +- .../05.workflow-components/05.inputs/index.md | 31 +++++- .../07.triggers/06.mcp-tool-trigger/index.md | 101 ++++++++++++++++++ .../07.triggers/index.mdx | 3 +- .../docs/06.concepts/system-labels/index.md | 14 +++ .../docs/07.enterprise/03.auth/rbac/index.md | 15 +++ src/contents/docs/ai-tools/ai-agents/index.md | 4 + src/contents/docs/ai-tools/index.mdx | 9 +- .../docs/ai-tools/mcp-server/index.md | 94 ++++++++++++++++ .../06.enterprise-and-advanced/index.md | 19 ++++ 10 files changed, 291 insertions(+), 5 deletions(-) create mode 100644 src/contents/docs/05.workflow-components/07.triggers/06.mcp-tool-trigger/index.md create mode 100644 src/contents/docs/ai-tools/mcp-server/index.md diff --git a/public/llms.txt b/public/llms.txt index 26d6bc51616..f7463771037 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -28,6 +28,7 @@ This section covers two distinct things: (1) guidance for AI agents interacting - [AI Agents](https://kestra.io/docs/ai-tools/ai-agents.md): Build autonomous orchestration patterns where agents decide which tasks to run based on runtime context - [AI Workflows](https://kestra.io/docs/ai-tools/ai-workflows.md): Patterns for building AI-native workflows — LLM calls, tool use, and multi-step inference pipelines — using Kestra tasks - [RAG Workflows](https://kestra.io/docs/ai-tools/ai-rag-workflows.md): Retrieval-augmented generation patterns — indexing, chunking, embedding, and querying — orchestrated as Kestra flows +- [MCP Server](https://kestra.io/docs/ai-tools/mcp-server.md): Expose Kestra flows as MCP tools for AI agents — configure MCP servers, connect Claude Desktop/Code/Cursor, and understand OSS vs EE auth options ## Authoring flows @@ -38,7 +39,8 @@ Use this section when writing, editing, or understanding the structure of a Kest - [Flowable tasks](https://kestra.io/docs/workflow-components/tasks/flowable-tasks.md): Control flow primitives — `Sequential`, `Parallel`, `ForEach`, `ForEachItem`, `Switch`, `If`, `DAG`, `LoopUntil`, `Subflow`, `AllowFailure`, `Pause`, `WorkingDirectory` - [Inputs](https://kestra.io/docs/workflow-components/inputs.md): Typed runtime parameters (`STRING`, `INT`, `BOOLEAN`, `FILE`, `JSON`, `ARRAY`, `ENUM`, `DATETIME`, etc.) with optional defaults and validation - [Outputs](https://kestra.io/docs/workflow-components/outputs.md): Reference task outputs with `{{ outputs.task_id.attribute }}`, dynamic task outputs with `{{ outputs.task_id[taskrun.value].attribute }}`, and sibling outputs inside loops -- [Triggers](https://kestra.io/docs/workflow-components/triggers.md): Start flows automatically — Schedule (cron), Flow (react to another flow's completion), Webhook, Polling, and Realtime triggers +- [Triggers](https://kestra.io/docs/workflow-components/triggers.md): Start flows automatically — Schedule (cron), Flow (react to another flow's completion), Webhook, Polling, Realtime, and MCP Tool triggers +- [MCP Tool Trigger](https://kestra.io/docs/workflow-components/triggers/mcp-tool-trigger.md): Register a flow as a named MCP tool — `toolName`, `title`, `toolDescription`, `mcpServer`, and `annotations` properties; flow inputs/outputs auto-mapped to JSON schema - [Variables](https://kestra.io/docs/workflow-components/variables.md): Flow-level named values referenced as `{{ vars.name }}`; useful for values reused across multiple tasks - [Subflows](https://kestra.io/docs/workflow-components/subflows.md): Call another flow as a task, pass inputs, wait for completion, and consume its outputs - [Errors](https://kestra.io/docs/workflow-components/errors.md): `errors` block for flow-level error handling tasks; `AllowFailure` for marking individual tasks as non-fatal @@ -197,6 +199,7 @@ Every page in the Kestra documentation. Use this section to enumerate all availa - [AI Copilot in Kestra – Generate and Edit Flows](https://kestra.io/docs/ai-tools/ai-copilot.md) - [RAG Workflows in Kestra – Retrieval-Augmented Generation](https://kestra.io/docs/ai-tools/ai-rag-workflows.md) - [AI Workflows in Kestra: Orchestrate with Any LLM](https://kestra.io/docs/ai-tools/ai-workflows.md) +- [MCP Server in Kestra – Expose Flows as AI Tools](https://kestra.io/docs/ai-tools/mcp-server.md) - [API Reference: Enterprise and Open Source Editions](https://kestra.io/docs/api-reference.md) - [Cloud & Enterprise API Reference for Kestra](https://kestra.io/docs/api-reference/enterprise.md) - [SDK Language Clients for the Kestra API](https://kestra.io/docs/api-reference/kestra-sdk.md) @@ -654,4 +657,5 @@ Every page in the Kestra documentation. Use this section to enumerate all availa - [Realtime Trigger in Kestra – Millisecond Eventing](https://kestra.io/docs/workflow-components/triggers/realtime-trigger.md) - [Schedule Trigger in Kestra – Cron-Based Scheduling](https://kestra.io/docs/workflow-components/triggers/schedule-trigger.md) - [Webhook Trigger in Kestra – Start Flows via HTTP](https://kestra.io/docs/workflow-components/triggers/webhook-trigger.md) +- [MCP Tool Trigger in Kestra – Expose Flows as AI Tools](https://kestra.io/docs/workflow-components/triggers/mcp-tool-trigger.md) - [Variables in Kestra – Reuse Values Across Flows](https://kestra.io/docs/workflow-components/variables.md) diff --git a/src/contents/docs/05.workflow-components/05.inputs/index.md b/src/contents/docs/05.workflow-components/05.inputs/index.md index 1d178289c90..dbd6fd4b910 100644 --- a/src/contents/docs/05.workflow-components/05.inputs/index.md +++ b/src/contents/docs/05.workflow-components/05.inputs/index.md @@ -170,7 +170,7 @@ Here is the list of supported data types: - `TIME`: Must be a valid full [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) time without the timezone from a text string such as `10:15:30`. - `DURATION`: Must be a valid full [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) duration from a text string such as `PT5M6S`. - `FILE`: Either a file uploaded at execution time as `Content-Type: multipart/form-data` with `Content-Disposition: form-data; name=""; filename=""` (where `` is the input name and `` is the original filename of the file being uploaded), or a default file referenced via the universal file protocol using `nsfile:///path/to/file` (namespace file) or `file:///path/to/file` (local file from an allowed path). `FILE` type inputs also have the `allowedFileExtensions` property to control which types of files can be uploaded. -- `JSON`: Must be a valid JSON string and will be converted to a typed form. +- `JSON`: Must be a valid JSON string and will be converted to a typed form. Accepts an optional `jsonSchema` property (JSON Schema Draft 2020-12) to validate the structure of the input value at execution time. - `YAML`: Must be a valid YAML string. - `URI`: Must be a valid URI and will be kept as a string. - `SECRET`: Encrypted string stored in the database. It is decrypted at runtime and can be used in all tasks. The value of a `SECRET` input is masked in the UI and in the execution context. Note that you need to set the [encryption key](../../configuration/05.security-and-secrets/index.md) in your [Kestra configuration](../../configuration/index.mdx) before using it. @@ -205,6 +205,35 @@ Kestra validates the `type` of each input. In addition to the type validation, s - `DATE`: `after` and `before` properties help you ensure that the input value is within the allowed date range. - `TIME`: `after` and `before` properties help you ensure that the input value is within the allowed time range. - `DATETIME`: `after` and `before` properties help you ensure that the input value is within the allowed date and time range. +- `JSON`: A `jsonSchema` property accepts a JSON Schema Draft 2020-12 string. If provided, the input value is validated against the schema at execution time. If the value does not conform, the execution is rejected before it starts. + +### Example: use JSON schema validation + +```yaml +id: json_schema_validation +namespace: company.team + +inputs: + - id: payload + type: JSON + jsonSchema: | + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "required": ["name"], + "properties": { + "name": { "type": "string" } + }, + "additionalProperties": false + } + +tasks: + - id: log + type: io.kestra.plugin.core.log.Log + message: "Hello, {{ inputs.payload.name }}!" +``` + +If you pass `{"name": 42}`, the execution will be rejected with a constraint violation before any task runs. If you pass `{"name": "Alice"}`, the flow proceeds normally. ### Example: use input validators in your flows diff --git a/src/contents/docs/05.workflow-components/07.triggers/06.mcp-tool-trigger/index.md b/src/contents/docs/05.workflow-components/07.triggers/06.mcp-tool-trigger/index.md new file mode 100644 index 00000000000..43427f2c1f6 --- /dev/null +++ b/src/contents/docs/05.workflow-components/07.triggers/06.mcp-tool-trigger/index.md @@ -0,0 +1,101 @@ +--- +title: MCP Tool Trigger in Kestra – Expose Flows as AI Tools +h1: Expose Flows as MCP Tools with the McpToolTrigger +description: Use the McpToolTrigger to expose Kestra flows as tools on an MCP server. AI agents can discover and invoke them automatically, with inputs and outputs mapped to a JSON schema. +sidebarTitle: MCP Tool Trigger +icon: /src/contents/docs/icons/flow.svg +version: ">= 2.0.0" +--- + +Expose a flow as a named tool on a Kestra MCP server. + +The `McpToolTrigger` makes any flow discoverable and callable by MCP-compatible AI agents such as Claude Desktop, Claude Code, and Cursor. Flow inputs are automatically converted to a JSON schema tool spec so the AI agent knows exactly what parameters to pass. Each invocation creates a new flow execution tagged with `system.from:mcp` for observability. + +```yaml +type: io.kestra.plugin.core.trigger.McpToolTrigger +``` + +:::alert{type="info"} +Every tenant has a `default` MCP server provisioned on startup, so the trigger works without creating a server first. See [MCP Server](../../../ai-tools/mcp-server/index.md) to create additional servers and connect AI agent clients. +::: + +## Example + +```yaml +id: hello_world +namespace: company.team + +inputs: + - id: user + type: STRING + defaults: John Doe + description: "The name of the user to greet." + +tasks: + - id: greet + type: io.kestra.plugin.core.output.OutputValues + values: + greeting: "Hello, {{ inputs.user }}!" + +outputs: + - id: greeting + type: STRING + value: "{{ outputs.greet.values.greeting }}" + +triggers: + - id: mcp + type: io.kestra.plugin.core.trigger.McpToolTrigger + toolName: hello_world + title: Hello World greeting tool + toolDescription: Returns a personalised greeting. Call this when the user asks for a greeting. + mcpServer: default +``` + +When deployed, an MCP client connected to the `default` server will discover a tool named `hello_world`. It will accept a `user` parameter (typed as `string` from the flow input) and return a `greeting` string in the tool response. + +## Properties + +| Property | Required | Default | Description | +|---|---|---|---| +| `toolName` | Yes | — | Tool identifier shown to the AI agent. Must match `^[a-z0-9][a-z0-9_-]*$`. | +| `title` | Yes | — | Human-readable name shown to the AI agent. | +| `toolDescription` | No | — | Description of the tool shown to the AI agent, used to decide when to invoke it. | +| `mcpServer` | No | `"default"` | ID of the MCP server to register this tool on. Must match the `id` of an existing [MCP server](../../../ai-tools/mcp-server/index.md). | +| `annotations.readOnly` | No | `false` | Hint that this tool does not modify its environment. | +| `annotations.destructive` | No | `true` | Hint that this tool may perform destructive updates. Only meaningful when `readOnly` is `false`. | +| `annotations.openWorld` | No | `false` | Hint that this tool may interact with entities outside its closed domain. | +| `annotations.idempotent` | No | `false` | Hint that calling the tool repeatedly with the same arguments has no additional effect. Only meaningful when `readOnly` is `false`. | + +Annotations are informational hints for MCP clients. They do not affect execution behavior. + +A flow can be registered on exactly one MCP server at a time via the `mcpServer` property. Multiple flows can share the same server, each appearing as a separate tool. + +### Writing effective tool descriptions + +The `toolDescription` is what the AI agent reads to decide whether to call your tool. Describe *when* and *why* to invoke the flow, not just what it does. For example: + +```yaml +toolDescription: > + Returns a personalised greeting for a named user. + Call this tool whenever the user asks to be greeted or wants a welcome message. +``` + +## Input and output mapping + +Flow inputs and outputs are automatically mapped to the MCP tool's input and output schema. + +- Each flow `input` becomes a tool parameter. The `description` field on the input is passed to the AI agent as the parameter description. +- Flow `outputs` are returned in the tool response. Each output's `displayName` is used as the label in the response. + +To constrain the structure of a `JSON`-type input, use the `jsonSchema` property. See [JSON input validation](../../05.inputs/index.md#input-validation). + +## Observability + +Every execution created via MCP carries two [system labels](../../../06.concepts/system-labels/index.md): + +| Label | Value | +|---|---| +| `system.from` | `mcp` | +| `system.mcpServerId` | The `id` of the MCP server that invoked the tool | + +Filter executions by `system.from: mcp` in the Executions view to see all MCP-triggered runs. diff --git a/src/contents/docs/05.workflow-components/07.triggers/index.mdx b/src/contents/docs/05.workflow-components/07.triggers/index.mdx index 65e6e7ef0ca..b7eaffbaeac 100644 --- a/src/contents/docs/05.workflow-components/07.triggers/index.mdx +++ b/src/contents/docs/05.workflow-components/07.triggers/index.mdx @@ -25,13 +25,14 @@ Triggers can be either scheduled or event-based. ## Trigger types -Kestra supports five core trigger types: +Kestra supports six core trigger types: - [Schedule trigger](./01.schedule-trigger/index.md) allows you to execute your flow on a regular cadence e.g. using a CRON expression and custom scheduling conditions. - [Flow trigger](./02.flow-trigger/index.md) allows you to execute your flow when another flow finishes its execution (based on a configurable list of states). - [Webhook trigger](./03.webhook-trigger/index.md) allows you to execute your flow based on an HTTP request emitted by a webhook. - [Polling trigger](./04.polling-trigger/index.md) allows you to execute your flow by polling external systems for the presence of data. - [Realtime trigger](./05.realtime-trigger/index.md) allows you to execute your flow when events happen with millisecond latency. +- [MCP Tool trigger](./06.mcp-tool-trigger/index.md) allows you to expose your flow as a named tool on a Kestra MCP server, making it callable by AI agents such as Claude Desktop, Claude Code, and Cursor. Many other triggers are available from the plugins, such as triggers based on file detection events, e.g. the [S3 trigger](/plugins/plugin-aws/s3/io.kestra.plugin.aws.s3.trigger), or a new message arrival in a message queue, such as the [SQS](/plugins/plugin-aws/sqs/io.kestra.plugin.aws.sqs.realtimetrigger) or [Kafka trigger](/plugins/plugin-kafka/io.kestra.plugin.kafka.trigger). diff --git a/src/contents/docs/06.concepts/system-labels/index.md b/src/contents/docs/06.concepts/system-labels/index.md index ad533e022cb..5755329767b 100644 --- a/src/contents/docs/06.concepts/system-labels/index.md +++ b/src/contents/docs/06.concepts/system-labels/index.md @@ -84,3 +84,17 @@ Once this label is set, the editor for this flow will be disabled in the UI. :::alert{type="info"} In the Enterprise Edition, updating a read-only flow server-side is restricted to service accounts or API keys. ::: + +--- + +### `system.from` + +- Automatically set on every execution created by a Kestra MCP server +- Value is always `mcp` +- Use this label to filter all executions triggered by AI agents via the [McpToolTrigger](../../05.workflow-components/07.triggers/06.mcp-tool-trigger/index.md) + +### `system.mcpServerId` + +- Automatically set on every execution created by a Kestra MCP server +- Value is the `id` of the MCP server that invoked the tool +- Use this label together with `system.from: mcp` to identify which server triggered a specific execution diff --git a/src/contents/docs/07.enterprise/03.auth/rbac/index.md b/src/contents/docs/07.enterprise/03.auth/rbac/index.md index 33cf4f0e873..df48b9ebcc5 100644 --- a/src/contents/docs/07.enterprise/03.auth/rbac/index.md +++ b/src/contents/docs/07.enterprise/03.auth/rbac/index.md @@ -74,6 +74,7 @@ A Permission is a resource that can be accessed by a User or Group. Open the fol - `APP` - `AI_COPILOT` - `APPEXECUTION` +- `MCP_SERVER` - `TEST` - `ASSET` - `USER` @@ -107,6 +108,20 @@ Example (Flows): For a complete CRUD-to-endpoint mapping for every permission, see the [Permissions Reference](./permissions-reference/index.md). ::: +### MCP server permissions + +`MCP_SERVER` is a first-class RBAC resource that controls access to [Kestra MCP servers](../../../ai-tools/mcp-server/index.md). Supported actions are `VIEW`, `LIST`, `CREATE`, `UPDATE`, and `DELETE`. + +Default role assignments: + +| Role | Actions granted | +|---|---| +| Admin | All (`VIEW`, `LIST`, `CREATE`, `UPDATE`, `DELETE`) | +| Editor / Developer | All (`VIEW`, `LIST`, `CREATE`, `UPDATE`, `DELETE`) | +| Viewer | `VIEW`, `LIST` | + +In addition to these permissions, access to a **private** MCP server is also flow-scoped: a user can connect to a private server only if they have `FLOW.EXECUTE` on at least one namespace that contains a flow with an `McpToolTrigger` pointing at that server. + ### Currently supported roles Currently, Kestra only creates an **Admin** role by default. That role grants full access to **all resources**. diff --git a/src/contents/docs/ai-tools/ai-agents/index.md b/src/contents/docs/ai-tools/ai-agents/index.md index 0cd179a3461..046bae864de 100644 --- a/src/contents/docs/ai-tools/ai-agents/index.md +++ b/src/contents/docs/ai-tools/ai-agents/index.md @@ -223,9 +223,13 @@ A single `Skill` tool can define multiple skills. Each skill must have a unique ### MCP clients +Kestra supports MCP in two directions. These clients cover the **Kestra-as-client** direction: your flow calls tools on an *external* MCP server. For the opposite direction — exposing your flows *as* MCP tools for external AI agents to call — see [MCP Server](../mcp-server/index.md) and the [McpToolTrigger](../../05.workflow-components/07.triggers/06.mcp-tool-trigger/index.md). + Connect the agent to any [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server to expose its tools: - [**DockerMcpClient**](/plugins/plugin-ai/tool/dockermcpclient) — runs an MCP server inside a Docker container. - [**SseMcpClient**](/plugins/plugin-ai/tool/ssemcpclient) — connects to a remote MCP server over Server-Sent Events (SSE). - [**StdioMcpClient**](/plugins/plugin-ai/tool/stdiomcpclient) — spawns a local MCP server process and communicates over stdio. - [**StreamableHttpMcpClient**](/plugins/plugin-ai/tool/streamablehttpmcpclient) — connects to an MCP server over HTTP streaming. + +The [Kestra Python MCP server](https://github.com/kestra-io/mcp-server-python) is an example of an external MCP server you can connect to from a Kestra AI Agent task using one of the clients above. diff --git a/src/contents/docs/ai-tools/index.mdx b/src/contents/docs/ai-tools/index.mdx index b7d008e8c5a..5b1761d08a3 100644 --- a/src/contents/docs/ai-tools/index.mdx +++ b/src/contents/docs/ai-tools/index.mdx @@ -12,7 +12,7 @@ Create, refine, and orchestrate workflows using natural language or autonomous d ## Learn how Kestra AI tools accelerate orchestration -Kestra provides two AI-powered features — **AI Copilot** and **AI Agents** — that extend how workflows can be created and executed. Additionally, **Agent Skills** let you bring Kestra expertise to external AI coding agents. +Kestra provides AI-powered features — **AI Copilot**, **AI Agents**, and **MCP Server** — that extend how workflows can be created and executed. Additionally, **Agent Skills** let you bring Kestra expertise to external AI coding agents. ## AI Copilot @@ -22,6 +22,10 @@ AI Copilot allows users to generate and refine flow definitions from natural lan AI Agents provide autonomous orchestration capabilities. An AI Agent task uses a large language model (LLM), optional memory, and configured tools such as web search, task execution, or flow calling. The agent can dynamically decide which actions to take, loop until conditions are satisfied, and adapt based on new information. Unlike static flows that follow a fixed sequence, agents operate adaptively while remaining observable and fully defined as code. +## MCP Server + +The Kestra MCP Server exposes flows as tools for MCP-compatible AI agents. Add an `McpToolTrigger` to any flow and it is automatically registered as a named tool on a Kestra MCP server. AI agents such as Claude Desktop, Claude Code, and Cursor can then discover and invoke your flows directly, with flow inputs and outputs mapped to a JSON schema. + ## Agent Skills Agent Skills are structured knowledge files that teach external AI coding agents — such as Claude Code, Cursor, and Windsurf — how to generate Kestra flows and operate Kestra environments using `kestractl`. Unlike AI Copilot (which works inside the Kestra UI) or AI Agents (which run inside flows), Agent Skills bring Kestra expertise directly to your editor or terminal. @@ -32,8 +36,9 @@ Together, these approaches offer complementary ways to work with AI: - **AI Copilot**: speeds up flow creation and modification by translating natural language instructions into YAML. - **AI Agents**: enable adaptive orchestration patterns where task sequences are not predetermined but are chosen dynamically at runtime. +- **MCP Server**: exposes flows as callable tools for external AI agents, making Kestra orchestration available to any MCP-compatible client. - **Agent Skills**: give external AI coding agents structured knowledge to generate valid Kestra flows and operate environments from your development tools. -AI Copilot and AI Agents are built into Kestra, while Agent Skills extend Kestra expertise to the external tools you already use. +AI Copilot, AI Agents, and MCP Server are built into Kestra, while Agent Skills extend Kestra expertise to the external tools you already use. \ No newline at end of file diff --git a/src/contents/docs/ai-tools/mcp-server/index.md b/src/contents/docs/ai-tools/mcp-server/index.md new file mode 100644 index 00000000000..6775061de41 --- /dev/null +++ b/src/contents/docs/ai-tools/mcp-server/index.md @@ -0,0 +1,94 @@ +--- +title: MCP Server in Kestra – Expose Flows as AI Tools +h1: Configure Kestra MCP Servers and Connect AI Agents +description: Configure Kestra MCP servers to expose flows as tools for AI agents. Learn how to create servers, set authentication, and connect Claude Desktop, Claude Code, and Cursor. +sidebarTitle: MCP Server +icon: /src/contents/docs/icons/ai.svg +version: "2.0.0" +editions: ["OSS", "EE"] +--- + +A Kestra MCP server exposes flows as named tools over HTTP for AI agents to discover and call. + +A Kestra MCP server is a tenant-scoped entity that uses the [Model Context Protocol](https://modelcontextprotocol.io). Any flow with an [`McpToolTrigger`](../../05.workflow-components/07.triggers/06.mcp-tool-trigger/index.md) is automatically registered as a named tool on its target server. AI agents discover the tool list at connection time, so adding or removing triggers takes effect without restarting clients. + +## Two directions: Kestra as server vs. Kestra as client + +Kestra supports MCP in both directions: + +| Direction | How | When to use | +|---|---|---| +| **Kestra as MCP server** | `McpToolTrigger` + MCP server entity | AI agents (Claude, Cursor) call your flows as tools | +| **Kestra as MCP client** | MCP client tasks (`SseMcpClient`, `StreamableHttpMcpClient`, `StdioMcpClient`, `DockerMcpClient`) | Your flows call external MCP servers as part of an AI Agent task | + +This page covers Kestra as an MCP server. For using external MCP servers from within flows, see [AI Agents](../ai-agents/index.md). + +## Default server + +A `default` MCP server is automatically provisioned for every tenant on startup. You can use it immediately — no setup needed. The `McpToolTrigger`'s `mcpServer` property defaults to `"default"`, so a minimal trigger requires no explicit server reference. + +## Managing MCP servers + +Navigate to **Tenant → MCP Servers** in the left sidebar to view, create, edit, and manage MCP servers. + +Each server has the following fields: + +| Field | Description | +|---|---| +| `name` | Display name for the server. | +| `description` | Optional description shown in the UI. | +| `systemPrompt` | Instructions prepended to every AI agent session connected to this server. Use this to guide agent behavior — for example, to restrict which tools to call or define the agent's persona. | +| `serverType` | `PRIVATE` (default) or `PUBLIC`. A private server requires authentication; a public server accepts unauthenticated connections. | +| `authType` | `BASIC` (username/password, available in OSS and EE) or `API_TOKEN` (EE only). | + +### Authentication types + +| Auth type | Available in | Notes | +|---|---|---| +| `BASIC` | OSS, EE | Username and password required on connect. | +| `API_TOKEN` | EE only | API token required on connect. Rejected on OSS. | +| OAuth2 | EE only | Required for browser-based MCP clients such as Claude web. Not yet available. | + +Keep servers private unless you have a specific reason to expose them publicly. A public server allows any MCP client to call any flow registered on it without authentication. + +## Connecting an AI agent client + +Open a server in the UI and click the **Connect** tab: + +- The SSE endpoint URL for the server +- Ready-to-paste configuration snippets for: + - **Claude Desktop** — JSON block to add to `claude_desktop_config.json` + - **Claude Code** — `claude mcp add` command to run in your terminal + - **Cursor** — server URL to paste into Cursor Settings → MCP → Add new MCP server + - **Codex** — connection configuration + +## Viewing registered tools + +The **Tool Flows** tab on each server lists all flows that have an `McpToolTrigger` pointing at that server. Use this to audit which flows are exposed and to navigate directly to a flow's trigger configuration. + +## RBAC (Enterprise) + +In the Enterprise Edition, `MCP_SERVER` is a first-class RBAC resource. See [RBAC](../../07.enterprise/03.auth/rbac/index.md#mcp-server-permissions) for the default role assignments. + +Access to a private server is also flow-scoped: a user can connect to a private MCP server only if they have `FLOW.EXECUTE` permission on at least one namespace that has a flow with an `McpToolTrigger` pointing at that server. + +## MCP server cache configuration + +By default, each webserver node caches MCP server configuration in memory and hot-reloads it when a server is created, updated, or deleted. Two optional properties control the cache behavior: + +| Property | Default | Description | +|---|---|---| +| `kestra.mcp.server-cache-config.maximum-size` | `500` | Maximum number of MCP server entries to cache. | +| `kestra.mcp.server-cache-config.expire-after-access` | `PT5M` | How long a cache entry remains valid after last access. | + +Example configuration: + +```yaml +kestra: + mcp: + server-cache-config: + maximum-size: 200 + expire-after-access: PT10M +``` + +Adjust these only if you have a large number of MCP servers or tight memory constraints. The defaults are sufficient for most deployments. diff --git a/src/contents/docs/configuration/06.enterprise-and-advanced/index.md b/src/contents/docs/configuration/06.enterprise-and-advanced/index.md index 5406cc6b1d5..ae2293a6fc4 100644 --- a/src/contents/docs/configuration/06.enterprise-and-advanced/index.md +++ b/src/contents/docs/configuration/06.enterprise-and-advanced/index.md @@ -412,6 +412,25 @@ kestra: If indexing falls behind, tune indexer batch settings before changing flow definitions. Those settings control how aggressively Kafka-backed events are flushed into Elasticsearch. +## MCP server cache + +Each webserver node caches MCP server configuration in memory and hot-reloads it when a server is created, updated, or deleted. Two properties control this cache: + +| Property | Default | Description | +|---|---|---| +| `kestra.mcp.server-cache-config.maximum-size` | `500` | Maximum number of MCP server entries held in the cache. | +| `kestra.mcp.server-cache-config.expire-after-access` | `PT5M` | Duration after which a cache entry expires if not accessed. | + +```yaml +kestra: + mcp: + server-cache-config: + maximum-size: 200 + expire-after-access: PT10M +``` + +Tune these only if you have a large number of MCP servers or tight memory constraints. The defaults are sufficient for most deployments. + ## AI and isolated environments These are the most optional settings on the page. They matter only if you are enabling Copilot integrations or operating Kestra in restricted network environments. From e439b6bba00144b75965f4d0bf130c04612204f1 Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Thu, 28 May 2026 10:52:24 +0200 Subject: [PATCH 49/52] docs(file-uploads): add to local file access page and config Part of https://github.com/kestra-io/plugin-fs/issues/310 --- .../access-local-files/index.md | 98 ++++++++++++++++++- .../02.runtime-and-storage/index.md | 18 ++++ 2 files changed, 115 insertions(+), 1 deletion(-) diff --git a/src/contents/docs/15.how-to-guides/access-local-files/index.md b/src/contents/docs/15.how-to-guides/access-local-files/index.md index 0a433998ac4..acae1157d81 100644 --- a/src/contents/docs/15.how-to-guides/access-local-files/index.md +++ b/src/contents/docs/15.how-to-guides/access-local-files/index.md @@ -6,7 +6,7 @@ stage: Getting Started topics: - Scripting - Integrations -description: Access and process files stored on your local machine within Kestra workflows using bind mounts and the Process task runner. +description: Access files stored on your local machine within Kestra workflows using bind mounts, and batch-upload files to the local filesystem using the local.Uploads task. --- Access locally stored files on your machine inside Kestra workflows. @@ -62,3 +62,99 @@ tasks: commands: - cat /files/myfile.txt ``` + +## Batch-uploading files with `local.Uploads` + +[`io.kestra.plugin.fs.local.Uploads`](/plugins/plugin-fs/local/io.kestra.plugin.fs.local.uploads) writes multiple Kestra internal storage files to a directory on the local filesystem in a single task. It mirrors the `Uploads` task available on the FTP, FTPS, SFTP, and SMB backends. + +### Configure allowed paths + +Both [`local.Upload`](/plugins/plugin-fs/local/io.kestra.plugin.fs.local.upload) (single file) and `local.Uploads` (batch) require the destination directory to be listed in the plugin's `allowed-paths` configuration. Add the following to your `kestra.yml`: + +```yaml +kestra: + plugins: + configurations: + - type: io.kestra.plugin.fs.local.Uploads + values: + allowed-paths: + - /data/uploads + - type: io.kestra.plugin.fs.local.Upload + values: + allowed-paths: + - /data/uploads +``` + +Without this, any write to `/data/uploads` is rejected with a `SecurityException` even if the path is bind-mounted into the container. + +### Upload a list of files + +Pass a list of Kestra internal storage URIs to `from`. Each file is written to the `to` directory using its original filename. + +The flow below runs a data ingestion job that produces run logs and SQL migration scripts, then archives the logs to a local directory: + +```yaml +id: archive_pipeline_logs +namespace: company.team + +tasks: + - id: run_pipeline + type: io.kestra.plugin.scripts.shell.Commands + taskRunner: + type: io.kestra.plugin.core.runner.Process + outputFiles: + - "*.log" + - "*.sql" + commands: + - echo "ingested 1024 rows" > ingest.log + - echo "0 errors" > errors.log + - echo "ALTER TABLE orders ADD COLUMN status TEXT;" > schema.sql + - echo "INSERT INTO orders VALUES (1, 'pending');" > seed.sql + + - id: upload_logs + type: io.kestra.plugin.fs.local.Uploads + from: + - "{{ outputs.run_pipeline.outputFiles['ingest.log'] }}" + - "{{ outputs.run_pipeline.outputFiles['errors.log'] }}" + to: /data/uploads/logs +``` + +### Upload with custom destination filenames + +To rename files at the destination, pass a map of `destinationFilename: sourceURI` pairs instead of a list. This is useful for versioning — for example, tagging migration scripts with a version prefix before archiving them. + +In the flow above, replace the `upload_logs` task with: + +```yaml + - id: upload_migrations + type: io.kestra.plugin.fs.local.Uploads + from: + v1_schema.sql: "{{ outputs.run_pipeline.outputFiles['schema.sql'] }}" + v1_seed.sql: "{{ outputs.run_pipeline.outputFiles['seed.sql'] }}" + to: /data/uploads/migrations +``` + +### Filter by regular expression + +Use `regExp` to upload only files whose internal storage URI matches a pattern. Files that do not match are skipped. + +When a task produces a mixed set of outputs, `regExp` lets you route file types to separate destinations without splitting the upstream task. In the flow above, replace the `upload_logs` task with: + +```yaml + - id: upload_sql_only + type: io.kestra.plugin.fs.local.Uploads + from: + - "{{ outputs.run_pipeline.outputFiles['ingest.log'] }}" + - "{{ outputs.run_pipeline.outputFiles['errors.log'] }}" + - "{{ outputs.run_pipeline.outputFiles['schema.sql'] }}" + - "{{ outputs.run_pipeline.outputFiles['seed.sql'] }}" + regExp: ".*\\.sql$" + to: /data/uploads/migrations +``` + +### Additional properties + +| Property | Default | Description | +|---|---|---| +| `maxFiles` | `25` | Upper bound on how many files are written. Excess files are dropped with a warning. | +| `overwrite` | `true` | When `false`, the task fails if a destination file already exists. | diff --git a/src/contents/docs/configuration/02.runtime-and-storage/index.md b/src/contents/docs/configuration/02.runtime-and-storage/index.md index 7290c762ec3..ef42315d5f6 100644 --- a/src/contents/docs/configuration/02.runtime-and-storage/index.md +++ b/src/contents/docs/configuration/02.runtime-and-storage/index.md @@ -545,6 +545,8 @@ kestra: Use `html-head` sparingly for environment banners, extra CSS, or internal scripts that must load with the app shell. +### Local file access + To allow universal file access from host-mounted paths, both mount the directory and add it to the allowlist: ```yaml @@ -557,6 +559,22 @@ kestra: Without the allowlist, file-access URIs pointing at local host paths will be rejected even if the path is mounted into the container. +The `io.kestra.plugin.fs.local.Upload` and `io.kestra.plugin.fs.local.Uploads` tasks enforce their own `allowed-paths` check, independent of `kestra.local-files.allowed-paths`. Configure permitted directories under `plugins.configurations`: + +```yaml +kestra: + plugins: + configurations: + - type: io.kestra.plugin.fs.local.Uploads + values: + allowed-paths: + - /data/uploads + - type: io.kestra.plugin.fs.local.Upload + values: + allowed-paths: + - /data/uploads +``` + ## When to use this page - Need logs, telemetry, metrics, endpoints, CORS, or SSL: [Observability and Networking](../03.observability-and-networking/index.md) From d09b1db9e3b28d435ec215ef5c6e711fcf79347e Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Thu, 28 May 2026 13:52:48 +0200 Subject: [PATCH 50/52] docs(cloudrun): add waitUntilCompletion details Part of https://github.com/kestra-io/plugin-ee-gcp/issues/125 --- .../04.types/07.google-cloudrun-task-runner/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contents/docs/task-runners/04.types/07.google-cloudrun-task-runner/index.md b/src/contents/docs/task-runners/04.types/07.google-cloudrun-task-runner/index.md index 77d205309c7..126d677ea04 100644 --- a/src/contents/docs/task-runners/04.types/07.google-cloudrun-task-runner/index.md +++ b/src/contents/docs/task-runners/04.types/07.google-cloudrun-task-runner/index.md @@ -119,7 +119,7 @@ Three properties control how long the runner waits and how often it checks job s | Property | Default | Description | |---|---|---| -| `waitUntilCompletion` | `PT1H` | Maximum wall-clock time before the job is timed out. The task's own `timeout` takes precedence when set. | +| `waitUntilCompletion` | `PT1H` | Maps to the GCP **Task timeout** field visible in the GCP console under Task capacity. Controls both the GCP-enforced task timeout and the Kestra polling timeout — the Cloud Run task is forcibly terminated by GCP when this duration elapses. The Kestra task-level `timeout` property takes precedence when set. GCP maximum is 168 hours (`PT168H`). | | `completionCheckInterval` | `PT5S` | How often to poll the Cloud Run API for job status. Lower values reduce latency for short jobs; higher values reduce API calls for long ones. | | `waitForLogInterval` | `PT5S` | Extra time to stream late log entries after job completion. | From d09b7ccacc6671fb0373e59872fea73d8c2439d3 Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Fri, 29 May 2026 16:07:26 +0200 Subject: [PATCH 51/52] docs(unit-tests): expectedState property Part of https://github.com/kestra-io/kestra-ee/issues/7544 --- .../02.governance/unit-tests/index.md | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/contents/docs/07.enterprise/02.governance/unit-tests/index.md b/src/contents/docs/07.enterprise/02.governance/unit-tests/index.md index f1d9af0ee7c..fa68bd28f0f 100644 --- a/src/contents/docs/07.enterprise/02.governance/unit-tests/index.md +++ b/src/contents/docs/07.enterprise/02.governance/unit-tests/index.md @@ -34,10 +34,11 @@ The following diagram illustrates the structure of flows and unit tests together ## Configuration -Unit tests are written in YAML like flows. A test is made up of `testCases`, and each test case is made up of `fixtures` and `assertions`. Fixtures can target **files**, **inputs**, **tasks**, or **triggers** depending on what you need to mock or override. Like flows, you can write unit tests as code, in No Code, or with the [AI Copilot](../../../ai-tools/ai-copilot/index.md). +Unit tests are written in YAML like flows. A test is made up of `testCases`, and each test case is made up of `fixtures`, `assertions`, and an optional `expectedState`. Fixtures can target **files**, **inputs**, **tasks**, or **triggers** depending on what you need to mock or override. Like flows, you can write unit tests as code, in No Code, or with the [AI Copilot](../../../ai-tools/ai-copilot/index.md). - A **fixture** refers to the setup required before a test runs, such as initializing objects or configuring environments, to ensure the test has a consistent starting state. - An **assertion** is a statement that checks if a specific condition is true during the test. If the condition is false, the test fails, indicating an issue with the code being tested, while true indicates the expectation is met. +- **expectedState** sets the terminal state the flow must reach for the test to pass. It defaults to `SUCCESS`; set it to `FAILED`, `WARNING`, `KILLED`, or any other valid state to test intentional failure paths. Common fixture types: - **files**: provide inline files or namespace file URIs the flow can read. @@ -419,6 +420,44 @@ In this example: This approach allows you to test the complete flow logic while avoiding the overhead and complexity of executing actual scripts during testing. +## Assert expected failure state + +Some flows are designed to fail when conditions are not met — for example, a validation guard that uses `io.kestra.plugin.core.execution.Fail` to reject invalid inputs. The `expectedState` property on a test case lets you assert that a flow ends in a specific terminal state. It defaults to `SUCCESS`; set it to `FAILED`, `WARNING`, `KILLED`, or any other valid state. + +The following flow fails when the supplied quantity is not positive: + +```yaml +id: order_validation +namespace: company.team + +inputs: + - id: quantity + type: INT + +tasks: + - id: validate_quantity + type: io.kestra.plugin.core.execution.Fail + condition: "{{ inputs.quantity <= 0 }}" + errorMessage: "Order quantity must be greater than zero" +``` + +The test asserts that passing a negative value causes the expected failure: + +```yaml +id: order_validation_tests +namespace: company.team +flowId: order_validation +testCases: + - id: invalid_quantity_should_fail + type: io.kestra.core.tests.flow.UnitTest + expectedState: FAILED + fixtures: + inputs: + quantity: -1 +``` + +When `expectedState` is set, the test passes only if the execution ends in exactly that state. If it ends in a different state, the test fails and reports both the expected and actual states. + ## Available assertion operators While the above example uses `isNotNull` and `contains` as assertion operators, there are many more that can be used when designing unit tests for your flows. The complete list is as follows: From 9766528386ad10c97139f45089ac864ed4c0fc87 Mon Sep 17 00:00:00 2001 From: AJ Emerich Date: Fri, 29 May 2026 16:11:13 +0200 Subject: [PATCH 52/52] docs(plugin-defaults): remove warning Part of https://github.com/kestra-io/kestra/issues/14471 --- .../09.plugin-defaults/index.md | 7 ------- .../09.plugin-defaults/warning.png | Bin 743309 -> 0 bytes 2 files changed, 7 deletions(-) delete mode 100644 src/contents/docs/05.workflow-components/09.plugin-defaults/warning.png diff --git a/src/contents/docs/05.workflow-components/09.plugin-defaults/index.md b/src/contents/docs/05.workflow-components/09.plugin-defaults/index.md index 2b024c79fc2..e69bb48f91c 100644 --- a/src/contents/docs/05.workflow-components/09.plugin-defaults/index.md +++ b/src/contents/docs/05.workflow-components/09.plugin-defaults/index.md @@ -68,13 +68,6 @@ pluginDefaults: In this example, Docker and Python configurations are defined once in `pluginDefaults` rather than repeated in every task. -:::alert{type="info"} -If you move required attributes into `pluginDefaults`, the UI code editor may show warnings about missing arguments, because defaults are only resolved at runtime. As long as `pluginDefaults` contains the relevant arguments, you can save the flow and ignore the warning displayed in the editor. - -![pluginDefaultsWarning](./warning.png) - -::: - ## Plugin defaults in a global configuration Plugin defaults can also be defined globally in your Kestra configuration, applying the same values across all flows. To centrally manage credentials for the `io.kestra.plugin.aws` plugin, add the following to your Kestra configuration: diff --git a/src/contents/docs/05.workflow-components/09.plugin-defaults/warning.png b/src/contents/docs/05.workflow-components/09.plugin-defaults/warning.png deleted file mode 100644 index 0e6fc02c8ccf5da89503556e08dff8a6bb717abd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 743309 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 z zfhYXb^(ceWk*t68E-fSLI_!Lp^>~D4a}X3Fe&?rrkDYXV=RUs?p7^ay9UH6bui3tI z1Wj&n$~nqN1(H|&xL7zKm(=z@*zk()UW&`aktKA=^`2$a3C=E)zp-LBHJ@`4nT+x}#Vt6c2di z7v~ALw7pqM=!>EepmF1so9ChmJ+pJK>pabU76sh#82+q;IZ{Xyj?qu@w^6HF5claj z)A#~9PiIz_NPLLoc)BOw$83DOb31kWzeBJ!rkkWazdTa3eMSH4koWh=66pIKrOH>n z2AxIofucH1OhPb#pmN>~Ffe#io2q3iA68Q)oy!{0EYFjw*|O={o0_4a9Qxv0FCu5b z&$N{0X&jtAAe)C2e66ED@-M5&xA!@ftn4fWnIVX!XdERJT;a_R$SH1l@tX z@^;78_d|SEcFtcCYqm`sJ{eSvfV?kG%U8ZrcEhInk$nDr-+1x^MQb^r6;)jYg6ssQ z6N+U~{#rR_)U4^$4`y4j2;s^QGI-WE-My4NNuw6;bx2mR4+^Z*qk&!ETWRBkk%W9X zta@TFC?g-Kjdo6@;r=0mbFWiVS=vB_qpa4l-Lw5=EL&6{$C+t&F650)dObU%N@%ic z<{3@;-_W~Eu^zoq(e+H#1K>Fd$qVGsgqf*ld>=T3&RiVl1Ee!hWwEWIhbRYhmZ{~} za^Nb#pEa(HKLQb)j7B*0MXCSG46_)wy$iQ7*wPNU)_b)ZJLF-yOY;DGA4^`8mSV3u z^qLN*9{K3NKZTsqk2nJyE_0MsloD^<^Lc^NGU;l=a_AAXL}J~MaQD`~ODaL)FZQjI zvY`YE+WdOTG#v(u`?S-_mckq>9mM&lnJY<{iKVkjX%;qkLf_IG z$BDx&ljADprATfVeSpRf_F?DDy1p1)^@`hN?YPya{F8*ZII7sOEi63F$bwx*b=!7s za-GLBmB?yefs4=ReBEqa2YE|$)l~Op*LFV}1U8Vjg=VfjOCg(kgS+ppo(RhzYzVQ8Fo!5i?pgV1w9Z2xMsO?z*|Q<*V64HqAJ~kK27Fyaons?c^#_CkOA6QPx8Bg}8Is%a0K+&vYl0^8_ zWnhk7k^{JDV(=cRwG@mn73!y4J;pbCSqL1}Me8bB$?(eQ5*L;C7+6K{v8TsT(fw1Y z`Q=@KZ%8WWCqs9>aFQJ#?Qt|i^7)5s7u3lkw^j;yA;|PSg7*AO&{_-mxpTwr3+h=6 zX&t+3ZWT_&$%pkjG@Sk~qpwS4;s3kQcT^JPrUjUv2`6n0O2fV!aq2*Eefrv$;VARU z`yoL9rv$vL{c3p^|GU@MGRPmN4W(89rw%O7=dl5cG`>)7!|2LM-NAH12NywVt}+wF zOb*PGSluABqNwblViEDd;9l;6jGJ*BsWmb`42$^}nBM!Zz;r&erc(q={r?v*?W`F1 zK5!CYw`%$+1U|s*W3dbxX~$=3-J?hz8q~CIA*i&_wWl;!b=q#v=SGM}FC)HN@gt}O z=;f%z+pzXzHcF=99!{%msJ)IrREAX>Duz(hytO?X21c}#<7JGKP4Zv3dSagE-;k4rSg|L1!B))0T3T-hz2aQvEn#$S;fHFGceIz0NSFBP z6isB1|FWOD>^s@@+b$R%4i(HE;J?1yr(76G2!eeXkO9M6n7<46$s4lRkS>m}a%fm4;179;O``803Xqug4DsiWA z@WISk$$y3>-HwIo6Na@59oh)X2E3G74l_MUSl!b*tEgW}OO#FZpW|eG-TZTlwL$cQ z3wJ5&W~vsd=>t5uh!iK`j)$C|ie+k8@f+~FaEqi_*v{}(rxCA@=K~N&=I+@+?l&s6 zk+(L88cfKDD_u_N7H-&Ncf77kjM7;wEJzL;>MZ$)#dDugbgKrK|2w z4o*D5%#bq3aI2EC>0!(?GOTu z&zHVCk)pmQCCN7Ufjcp6bDKS%gju<@9D7ugVonrId0_a1NGqGNhn}CKp*%Pugas3_ zZXaIz@SY%zyLirN@|GKviQtxId{>2t@&+9H|H`l*I!oWb85&@gd-2I z$wD5#WVn@mIWaGtZvw4oQpDe0d18?xQ%B%I@X9|5$x94Jl+3aJ1Okgk`do{OU<<-->5OIW{vyt$z> zob}=E)g}2vVv_>ud1nuMtJ^%#ue;{jJ*R-!4UaZEGgfx5^P7P4sARE~rB9g3-m0^b z2YneUW-^etly$M6gGZiCF|TVkUD}ZDE$SgaR;x_Q8HSgw4*3M<8&!y}`kHKdHoPp= zX)&s;f*%p*uG6M6M`n=s-L>F{2`gwhHgn1RgITA%-#q!BM%bw4{PyT!1#Oi*p206g z#fY;oKs+?$ij@K~2tI(Y!@5>05P;X+4M2CJeVc#@S$7ocsIsxu1}U{WAH&uJlyCL@ ze*m<1a{ddT1?CF=JD~LuW)VgUmuH!8$5l*Y_}vY@V_#?|7lt25oTfs9vN;R1I9YEU zfzXings>Ps8ECX`Eyh2j+k~pJLa8OF*E-(Cgm07r!@f7&-t^voO$ii`G!)&rQX0aK zV$n)F#WSoKYTw?rrQ*5IGG~;gd^f{5v?dA9uco7nkdI+&bnX=h`_z%Tf>|ar_@_N_ zDP#~j^+TYzvRq5lhjL%gEfAAg_NhyFCL?-u^XjSOnAt^*=`rOX7y4`zL@!?M7JE3Bl^bkj_!Vhj1{lHn&hV21`0fQ^MLs^ zyP+-9&;_5w_BBJkbPOpvPx;LgQnWurCdjpev^HhWYO3QqYM}R9n+S5royEKH?4XCUwo3CC7mDo8C|5%n zs70Jl^g#LDUpCfL3S7sO_m8OgvYJC5|{Zag)NT<&JtYJSy zx!-R4;suV1sr5e0-$piRX#@XJSMJb*NlTQJ+ATrkZ+C^j){4={^?BzfuLId}rTIcC z=3OcEzSmlYZ@#7LMW=&-)L9r}BAQ;lgg<+aS{IJAZ@A*bE(ut+7%bTKM*c3KuXP@c+bq?EjL{Per^82avaNfrAM@h# zN{!57w0S8u@>jxZ4sX(8PZ6v`*fYpm_($wBaZXH6_4#Qy5}QBoh@6pRkZn{_1K8zs zj-+kg41qa7Z`GWcXev1~$!?8x=dmhxTwVANB?A5JaAgoThHoUk?aDfs1zNs%0UZ}N zO>J5|4R@d&&99!V{w_i`CvnPCv)VkN2O_T>U2lR zA=RbzG++kGn1@rqp}NNf&5T-qVIakP-k_oD=<-^S8b+o(AjpDUIvm!cB*P9(WP>$Ngcz|WKar7)i{$5|6p#8UXrm1?p`xZ5 zr|0K3TPo5s^_@*j$)e$Arz@!V za|xY2-|&rJo{gQu()WG&jMrRB|3+q=pYpn5Bsft57*gc|EYFQRur9*QOt;439>{h_E`;!c~6Fb6^LS5(gUTqNc)GMq= zO1-je;Kt0NUd~_cKMLJVpd53ptguy}k|Oy58T(IBMk8U9T_q}r{zK0)0&Bxp{KpX- zb>APrAjxAiro@ugy8)>POAh{?^AVrYmLVpPv@n^@Mm@!nl1HtseB6#4_6rX^Q^U5C^=d5 zZ##SQ_V!XO_wT7139>>QYz2=HP@dLwm~Oh3{CFB(LhJP%>zhdyQ5h`4@|!5ha2aui5|(UQP)y*9Un%=d=I& zEgA&d5~r%sdG_lS%4gFx@eD8S8$nKUJshD~1%2&-MLHl+1MSZe#l3QVWfa`gJ?l9D z48K(@0$soB9rb7Do(qFkRDGE?8Kh8fHdBohnSdO@H+GAdn${Nl}y&yPz_IU7~9g^RB%d2Qm<9XmY_ zK1&5J{N%*{#Oyx3bNcTtAK4k**y0aXH(!p-mdnXk=R3~V{T zAK?|OM(nnM!3d*B!SvGQBYq)^IZno3R4Iq6>C_K|C?$z9zCI1VYN)i)0(Dr zSj_tfo{^O&HR$dcG>4MwLANHZ#e}e$rvF^Et|YVu_}n06cOApyDzK?h3(HyNz4ZTQyI)d9V3B=X2#e4R*T0suQg-c^t^P zSj^$OpORFmu73a(Q?3M|3k6wT*o6xX3~H zUlT=a+!j_B=!gSB6n<7E!aa{dw!bR|;3Bfau9bpCkHm;#Ma;++0N+!=EM}tL5zlJt zEoAvjy9!UmV_psp5+Ud&Rra3oszzKF-9Gufo$0+0qpKCjXnfaBd$iy zPT=xyU9ieBeEMm6o1OZuOhvKLqZMgTSZVj%XVi$#?TK)=VH0d;Qt6~U|2614y4^Bd zN7Bn|bOnS>?^s^pvO(H2;=6X@3{Yqy&9hkgtOVlGIjc1DmR#U4(!76`kdfv1cih35 z{KgB#pYb~qg8z2j#m&H#z5)Q(*tpa>k9Z}g^WK4eGOQ`{wQCo9^({W((9C`Qi`lJ7 z|Hgw{+_SbDzA@|JsJor_*iKU6Lz(yo7XHr04yyT)8Il~I4WIe@;D>CjR3ed@KEcQ3 z*+_)w&=Ez1?Ee)Ja`o?skcE3U8IS9i$rR9`L6vc>Y(!VdHb}|*gRy+X1a4ILRs}n`1&9xUun>gQ`-22eI^;MCf@S5+usMpD*lmNMCjpz zUf@#waIY+LWHiYkH0>kohO33LOjUMMohiSHPg$+m5_pxuV3r|)k0LtiEc8NJ z2+PL85yG9xBqU)m)3GXO9BbZE%S{POVH}kC>v#XqadVGm-=zE7;4c>5Rcd!Aavq~N zzrUn7)KNUt8UN9X)ii^2{n6U`Cq&J14Tb3j?No-1jLQHfJ6L@xf@8#lWw~ zj;#x>IC8)OXxarHxy4a*c1d~SJOJ*Ez>4TviR3Nm&fGkOoGOPwVZ)wqo-3nG!^vFGQU>aPTeCe<2qWhVnj^NSj_``*Du3tW*ajj!sJ zWMCQeKo{g3cHWQcB^2W@2E4mzlWjotYJu&~bGMnI$1Gv;r|*33c2rN0=qkVC7{Jum zZyhooR+2sBOgto2uSHUvc3%uH?wP&d-AHc7^8ty%IilV0OuOcJ2A5iL)oCB{ha4|< z7-$6hRjEBLQR?5e4JE7JkR7!Q%v+%Tawu%HOD_J~C{6fflm`5>p-R)D5uO>Neak2O z+7xC&Muks)%6{0L6(gqTeJAPbj2uG=(!%>>u@F8OATVTIw*Y1(s-Xt3fzNZG*8$Kz zc85Z%(wsw}<-bY(@$U=Fb?>fxklwZR|_ z=U4KwhKX2gq>m;6BgfUee0s^kI4X_zb|sVhc4b7)jKDQsv1HA~Ad4yGGZCika>0c5~{!c zu3n#43@olr2vdy&qlTzn^I5T&ZSeQuV1p8uE#b+uQb$#p{Pr~mlnjf1^`dtBIlHC7 zvJEt!MnjUlG-BaEz!ehM{gK^RyF-^u&VVxjKrD~R$&fbnkM z+y8@?-vxO&p>%Z|-= zZwWoNeOVE@e27RB3?i*lG(sS@h6u8sP%}YLtQ0bHwlYy`?kCfXFdqYzUYO#_Y1a0^ zZC^11!zbl_PvL7`tZBXHMvcFY0}IHWldFTyXbX^iYTjDry=EAZ0Tji}C=B@H=Uk7i zEL+2WnNXZkC+FyM2-Z13r~uqCG+k@h#kIGPWAV%(3u((B#Z;X_7U#28JJmTS zEFq+dJE4_(3NM@(t;BW$+Vq%y-LJ+MnqZDmElJk@);+lUHm(dkV?4J@f3DORh=Cwo zOxF7~=3&xFnoOr8^PO!9gMlMKG*Tb6xtuZQ)UX7^O?WzVEhqW^8d7fQy`mHo!vg0m zKv9i-p40zp)l0=JBcC9z6f3nJDn{{4wCY~uZuf%#9z8LqM_C1G#>1qdcUr?|fGPLQ zRu{HhhfnY7ROBh$h>Ps<93%}{+yUbz`o;6+>)0(_B+dgMEXvHD)sv_M8EU5)$!h3M z8(WbN9k*R>U8)@Zv$9^7!?-dR=Ea+p>T>b15UCfVl(}ooa&7!h|1rWe8#4&6Gm!?5 ztGmapyAQq->6hZ3h2fwTh&R2_EvkBbBvxT~0nC{Q#L5z4)Jvi(yEhBtmMJ}%XpGL6 zi3!L$()kbN4PpWKXGgXHnzko!4f`aMN0<)Em9!0Z{)}dinjm70FW0j~olalL>=+#tDE|>#o`~HG@2FdDIIz)5}teoZquasrd z^2B%knU5LPypx6SPOu0p6mG=O8N}=8r3PDgenGw;aMgkQ2R(S6Kftr!#FcOmR~6uM zDmjh=&pXeJ&qoS!kM{0PHt{pQslMBIZv|Zi!Q1vBxM9IonDdgw^V0U=_qXqN6iu1g z%MVH}-8^p0@ZJoRn0+I~a}b4rh^+93SF^%GaNZye|A?A^-+l z{-?{h*R|(i$acVG{OE6pef|BoXF-5lm7DYr=&uY1*~H%&4sZU=aHz7i46)j17}78T z1in-wGUW)wiudP!kVHxWoZiigR%lEdX>hXIUE=qxs8&s2=WjIsz)&B*-gb_AWwW7^ z12`Wg*d*Nt$u^=vEdB^fnX5-Lh*5ZkJet$wYhF16M%Ei04joTJe&mk9IWzRN$VnAb zku(5uO_YehD{ReR44d*txT1|=E5dL@1-FI9T`}5v$a;@^fL3=H@x`Qu%s-AeE|3h_ z<^1GOb>Z%c{VJQtd0EDaX|r=>^)W~UknIB4(orKey875Qo<4bl z;=!vW-I8(X3Y_ASN?7NL#)Z)swj=y@ObCY(XWKn8-YH)UdsLrKk6Dd2-gD)jm<7&x znN^(C($=om3c3#7g4jT_&$|B`m!PWBxevB4t=bBR=M2b{i@A>T zMvKoYGc7jHy#;eP0Yiqe>zN$9h<0Un_FVerHT#~FXO{9cu<|A{TdO)YN6*r}bMA0v zUNfdFB0QglJiW`)kP%=hZr(M-9@^IAuZSqI9G@@*8jd-1e#ygtrM9_QVGG0iX*P9x zzH46NFkJnGzKrm&W;f>Cai+ITTjFv^kX~0bQBO=f?w!3}PTkNt988WVHAC4H z4J%K(2Um1IQ}B9+YrNSBqK=l@xU~#PpTglo3=Pm0k7o}KngZ{7$^ndyCxuiREk?a8 z%c_KjX{k$AhnuT=Jlz=ZmEcOwHz#ALYqjm%<{@%~`**{!TzS;r_&IfpGewR!_h~WG z_tF}}7~R&}BvM^qg$wx;D#Hp#sXuD5|U`bAT%OXCnUFMF|MLlQ@vQ4s8_Kg}Y`03)u4#dSO}OIls=V2KSHs)NnpLzcLnaNh z1j8on4O8T;Z*$a3Yk4D%i}lVn!fg#lTp}b%$>&yes)fdYQrTrxQOzQKrnTp?@U-W} zA7uw--if8SutXlFZ@^@J)h%uy77<(ODofSPoSuWbie^J6B%SKAG8Dw;lNPZaEb{Si z+{neDimfpxFYA`v0zwTnEOENrpFK02nWYu1YyxM%HjU3ceFr652V_Rb-NjK0Io?r* z7L}rbXmCoyT~4Kon<=9oM16!+grIggb3d%}#CQy5TDZEJ?8OHdZ&J0pkNl zI@$IZa1Pw_>SF16v**Uw_*7XrYF!AQ^*t9+R#ZuPOUhsgzg+Mc`_T(D(2EnaXEli} z^|J>S>x5>+xAeYp>hRhxz?#a>zf&$P`$zT1-lPUa0%q5(uH~5sQm`3PuRZi(pNgs_ z_PuInCI9$o9|HGJ)KX}M%*PrE3t>+fDMU}-4BI_duDQs`Bd{XlnVG<>xSbGB5v*H z?%|c+&N4QyTnIu651XgmHmWD7Z<{b$?Fb^6PhC~}Vd2iwr0p%5K3QP2uHaVbqU~*% zkG*fv7Vn<&d&OV*$;kw^VsvpOkh*M{Ju<(`xA=#{5dyG$Yu)U?*PlG(uN(f-l#5H( z^^X*CDd$Z?tdJ5^p`C8-(_ zHcq?n@1gO{3c1z6odYq-Rh>`w!Nvpo_+6({>S>&Cz?jT(v=erCPA9ZRb7p7 z(1p1->-}1`E#_jx7Y|E>kPnNBL>d`3&U_B-OPeE4`#~DpgFUSWLH^sUk>c=WgkMg& zx;$-1^BQjr4@b7;SZg}2Aldor+M9+V)7bs5Z|#~+yi#fI?j-*yEdC<)isi-4+b=%9 z*ehT3K*2!_dG*%8pv+fO3G7>fHs%k%YKIv-vpoxY{$J^25xA4}j z^S6ucJ9DVjA&Tz(6=CdIr7z~JhYw$Ff6qE{?7R06P?nh-hsq6CRDYIxI?cbYOO|NX zqvg4&SD9bvN^=b|(C+^>(pdUXVP9{@S==8tKV6XgR-7KBvL|83R%Qng>i6Mo|DV@8 zMhxRU9sv0!_?;KyTC?SkmHD|S{qV^S^>$f>zQsF-)S}!HAcnlMLvO4NO5Q-14cMD< zW^ik+4Qj)5F~J~IeIES4j~nKnZ16t3bA8jIN(AA4q`*D!v_Im;%{TSHmp$2V zGUv%0CX`a0J^~qiQ3vmGNQyT`AnMNc|8aIDf_hT3VrZNqFE{})b9|w-$~~~C?Aex& zq_(M#yIz{;v|Cp^Oh_gic=@QV?(vIGcZ3AlNwwO0NH3qXrN#VH*)tQTaEq0)k4FrP z;kDR~>&Ty%#?6rGh~$~U&0S~jY&|M>bh+i{V&s^;%fLu9MR(h;pSh3txLWIxI%%E# z89}+^_u9FLlgUpOr`P1)pKR5t6H7ySzD0WK?0WYlD;$O7-@v()oY;rgb;U`LrXhpj0EP&3T|r-H-| zt;7HHofKD|#IP-VR0w~=2tRI#Jlsrj#rJIz0bx0&W^k0R;1bI56Xyy9f3W=|otnX) z7XE_Bw6b2eb$nwMuDB35dF7&7xABR5gx^#(rj^``DV5%6BY13kx&o`3e@Zw$0A}j+ z@Bid%0-uWLGZ1*6!$m!liGVH983V^M*=Kbr>w zEYb~cZ9=OE(-B`26|SAM>`i>U#kuH6l4QY>^=Ifw^y|@?hkGICrnUpg1 z;yX;zNx;~7hEV&(KO0?Add7BOE1%;aGn;?okABo@icNP_uI$sa(=A=2$+EGL=xg?& zsB$muJHxG_hCjR;W`shn1ue1ydi}pxN=U zTTI4v3mREx@jA#f<_=cv^e^jMy~qoq#(sF8cGSxaLS916=YKwhpcZAS=`Go31>M$C zhPJ=Gj+T%K^&xwk71ogC%=#AEJvd1e- z0R|?Y@3P8JWrxgoqYH~5>F=H5kC%&^qZRe}hS`Zps|diHs3JwVv#jNA=&rO)#HvvB zPX8*kYIaYqhX8hloV-Z?sZBjmzRxWV8=1utcUZ$oJvjR5a`YSbtFzF<3ZOu`;<_-t^rSf+;Iu3TVO@Ll(e=h zNs!Bsta7<(=gE_4pO@OctoEJz+d`1d70X+rTYR%0T>log;rX-O%N^revN<^FJE6c) zO5$P^UZs9}T&J8oF;(fwXsv#)*t$|IMh71Is$FhN^?t72ioeYG28+}5cbG%db?_#) zo-zv05|8oU5(0#ci^5f{IQ{qu``Kg@Zx@UQOnKa z`YGvKLcy2<>Y3rlmFN%9iNuc7M>|(_ePa1D1dMH(W--1OtO^w^$j1q6I(+jI39QB` z8Q#0n)K2|+*Oq?mb^US#ZTkg(Q|p*2?KhcY>CmO%6%m0coV&Bpqf@*kNWDHMb3(-1r$TEOjC%j zm-(b!H9}OrShdq-mm~L`9Q!uq-$$<3?I5VPoTaOz{hl~4VIca^7yEP&gy0RsJ8CtP zNkKZR5D8iHCsuW&IRB0{twJwdiNJi?oE>RXnwKnN5 zL!;xFRE(bqSp^BSh29;PDPPLAvE5Z_RlNaOkH(wjmZnnIH_|*Y3dFV#AEY!&{q)gK z`I_Ws5~o&FSW@Uhmr0yMjaZ8U%kEr5OR>PC4x)OabZRi=Ju7Tq^l1Eyd*o*V=#I2K zv!DKjQsO2b7e^iX8OL8%hu}2J_A^rJOImhGAxV9s&JBmcb7gNAnmPEoQyG=KKdt3+ zL1vAGJRUcXRC+oEmGh)D7Vhl|pV{#Q?#ryit|grD=|HXrz@j{XX7Fi-kH1K+qk6pt z^I|OcIb_h(q=2bn6_ttlhQ0=?XuXRv@vsu|y}!lnh=dB3=;ku3>`q!0xaF>Phik+> zf4!a2FFaXP0LVk3rtiA_BMBZgbsTI}g+|}_BA5~}xu`rD&h>eC$VK6R|L=&%##6jc8KhF$tphvcPLyLo>mp4I)7Vs*+A8g(Wvqr#{tu($SK(Z=I)5ze3oz< zk)UFVZdvR|58=QfDO*V@BbIOzU+hzsx6ueMz7I748oxb*ZN$i|?@+f#xS@~^ba~{o z(2&p#_0;gxu8A|E*EHCE6oJN3JEmC#N+3d?jKEr*JQkAlL>(a89Hk8>CI9wblKWVE zI0fVZdRlLnUS<_GShv^zZPQ+36Aql+(KqlQ--vm+cx>Z|%N&~M;+Ktj&AA=jd!kCr zELx<-0qNFN=1{S_{5L|%SIq$@83M%}GrF5tst%!>ILcd<`8Hkb&neBmG=V#oO$&%=@#HhCp-d7SA(z3;J)rY5yiv8-@p!Ku{$F z>BaMtddK1K-oka^o=WiIxcK_aiuua!(c{neb%rztVpC6KlY}@s(G=$rq^)Gp~xZny78ch;Dx&6!{yS+&_fxCS=TL2_3wiACJkBqlT|_A+Wz64t^>l49b*ZR5 ztOh25pIt#@j6U>}dsF2M@^GW7N-(HYuiMT}=cAN>_7GAO|7{(@d|A&CSA=FbkskY? z*wmNNU!hM|6@x#>G>(d0l712Hz0;fSk>*Ekm@v-YuBdM&)gACN^G5INv&xH!&Eb?T zGK8^Kes?zo-ve-qS)u+Vep$^l4Ij+<{%;HbqG(Td~td8)*FVX761{=pp_G;dLzH@xwFLvh5S#y+Zkjm>iwB4kjgx~ zKa2WWEAG~;=7$sOa4jEO5X3BO1Xe}-B;4}Q`@!Pi&^r!sv=Tq)X`!02(%{N4Z|qrn zWs!E&uf}Pt0RFvhp_JrD=Eb}7)P`>eij=cx=UV7q^Z zV}Q3X=unV6JoOLeJ+Kx$SafdruP|u}Sy>03R4A>7+~3_qV`d0{-)y;kM8;_lzf0#Dm}&GZbDT5Bn`3v#2k#>@ zzn~Up3XmqY`j~tfJ0Xz%|F)|2v)Tt`fP@bnPnS!w+hadID4y0bYiZoo3Z9S@7MNt} znV#W$*iEZAIFuDaubHs>SJFR>f?IxYw(aZ&m^k>6iCfDsPw{;tWbPxIPBAG8Ow&X* zuI@G~tI9YHzDZtaKJQZY!BA+rbd>f%@#R!dP5nM~*!R{sTljp9mSOJ|1)CW&Lhyv0 z5L*hV(t643fAU;U5L%10b%IEQ5;YkLyj|upj>+3m-F~@Z9OmgbIC`o(mCa6#?(&x6 zYw4G^)ejGFNm0AO`Sq@=0mZkCREMN9tB+?0{T&7YuMqmF*(cN>lcslqyB!Bi;&m2> z^oJpkp(xJ00(FcKdS{+=XTFM;Rfh0c3it_&($o0E3P$V{>V*xC9=lYMV1a3cun{p}8$?gjKD*L9IR)tljxuZ`hMHm3aFzh0gF3 ztV-#vjCSBa*fFYwI^}$o#>k&}$r5?D8kuP?`>OM7rf)z%OR1O#$UNb(pYk1TZR z^h)E(p)g62weh><{0dj(0YV<0i6y+OhNmw3O^`86LPDc)IMdWgrA~&EqT4Psg%13W z?Y-5J2$W~6rMH#*Njbf(T}p5kKN34Alh2I%O&XfRr2|eVIX$I_a@`w9V4ILm0H9=n z`?#7t(7i$L+qbX%w5LaINQ#M*_|xhDF1s;O30tqQ-1B->>JVqBNtxnGoD@2GC_3aB zohlrdN=T*TS&}rv6veTgR_;t|X$=^xL6VKyH0CoNj0wd4@G{$=xh=^+={82!+3?5q zjf`t&+Y-B*mjSv^e0vnVWaQ0`-RD2AB)1)>_$xjU8B?{1`IUeS`?YII?&Dz{;+w~a}2>ApXM za;xwBbO~QLE~y=lK5m})N>=!x8@Du95y&K&_ZRD9a@b4Fp8U7>xL%WEY|2h$+` zjWlV6k(vQ5acx8Jw#eOJk+*-HdYK=nK(mt2H2>+iD3JKTW`7Z5=*0yCn>)|=S0q(A z4$PrplO(60iNvmx&vsjeU_A%3n9kr&WDEa5?2)PTd0o~&t7j@^*Du%8HC z0i+i^mVz*gRmD(rEl>8xz1lP<|8z5HW~XCy3o%97)xK%} zwHfuzoY9%WK|(A46dkR;L1aVKFb`K%(hsf?(r{q5msnP9wz5T=+!I$fHfun^kD_Ff z^}F;n{DpvkdH9vn#t`1jP{XZoMm>d3WvHXlFKYa)U}DGW6LulBgSndUzZI}HydLPp z{;b1(Z2^;2gzD;reD~vb%KT(_x{!aU zgUCEDnfAMI=L`|Ib7C5g4*0;3b zz-nB)dp#&wae|K~aH=oB<3HDo{5n^IgtrbnJ>J0+hf^o0TGZs_H0YM<#NGDuK=;?F zae~q3^OS;(59WLNfNqCxYc(5Ds9wc?jKEuyc3k% zYTz@od zu3F{W5PTzps%!;@eMkWH!oNtAb7`VwNco@4!k?MsgIOCldd9!EE;hNNMu6v!!60Xs zuj5O$wGfgsVe>Y?f<<$e{q#8C2ft|hiOzoK-K14-;K!ej;onhG!6x@i{PeifSkQeB zW%=t>vuD3N`uS22aHXzJYK7?j%~8TkY9oj3NS%uJ0@NJ$&+I4haLW-fG>Jakl%ue1 z>75Md4$sqGK_l#O2XC)P1)$OET6mOf$f(+cc9gh4H$2CD)e;4+J<<`Px8>MgV!au z`=>qeF2nk;F@o*=nmtvT!MYiYz1?&Ot_+^^xZV-G(NL!8Awb{{>_wdA0T(Bhn}{k8 zT{^Cmt5ZN*<*6$rKCINsnN+R#>fu7ekNjhGrz?Y=8#c*c`O>eF9I-Mt7YhNJAy z*!_$KKZ9?&Y+jFm?n?^@>AX*?il`7Yjm5La`$Yu5tn{^&lhZZz+1jLgKe z9q+QLo_D!{inKB)(LhqS^u^A{(#V4q?%m0x3ULDCV3Y~O2dN#>rn=8DyKZ?MugbVQ z=cD^(bZf{CdZHc3HXJHn&fFMH;4jRBZ(R6wVFK-fxOl$7r9kR4&gFj3cA|nK=Er_R z_0ZR!$TiiS;{c~l{oNFeMa?#++SeZKGG6%z@JRpz557Ntd%CwOWOu2>W-2dB`u1Y} z2!1wdvrQvZjJljez~NC?O4yaeQN8b@8tXqsKY$*`8jV&R2PaQB%!~>$|EBktg5Eiw zAGpJ`3PvPx^$0un9~j@SX{%SdOlP=l&KFmtU4ce-X`Q|4-FP8{IT}fkq5Y;m zKu7_riI~lnGqLoEHD#e~1L8PgCOK=g@bgn(6T>kG_G%EC ztCx1YMKPV+EhZB?_Rp5#OZ--8>g^T-?R8(D%ljc$VvlA-;Dj|IH1v72{v!CBE9v~b zRtl@eh)o3x67x*4U3HiOI;Tq_w&>-S?Knri@=h{1vd@pQ?_OnMTRj}kxMCgL-*5#rPAEQg)x!E9w2 z+4dtARUD%fa-RZdgpFA53A(6Wdj(G{ery1=S2rz<1cZy*a@kL=bqUsnb5RvUql>I| zeAO;+BUtO@qx)Tc!o7P!7$mT}%G6sx;FxCHIg%pNJeQfp=GWyP(NtL!ES=*?ta zc=Iwsb46YDF`Ip-z+_F@0$lGT;LyS4`^-y_$d%PVOEjN152zN2-8guJ?drtXyH>X& z-HhGCG0}B-e3QdN02GTUZ#q#m^@@iDR5Gt;1>uqjrMTs|PL=mhf2>ETdle#T)0IPY zDx;Yqe5?tb`tr*rp-!YV{c5N167K=I z_D+%cV9kRj&vleN_4zPbMf_W!kfeiC!9l~0%0H(P7N#I`pb+@zF;^f$5n!Y@a>L-& zF#FF8k;y%hgPRq{QE;KeSyq9wy6ue-i8MJU(cmaFUDXHi2(bV53 zQX#jMC-{tJUA$K^RSHx27SE#`cz^2YqAZo~>&d$TyaCA$dAJVq`JxAPuvT6jw6%Ia zUp0zrHBedh-Btw8p4O?!*+lLmWloEGG)W3aV{VtMi!FK66ji_+*f>+mFS=Y5e79Q>cUPNDX+gQ|J~CAUe;sW(du{=dcdah3dP#J7In+ zT;(PlM|I=arXjd}RN<$rbe*MUG4-8PSt)0NgYi;~$=GPqvq7+Q}B z$MM_`LrXx45}*FeJ?_jK#IE1`Cg*EH*i#dmy4KMz2jzM`*O0tTB-PpMg_R@5Lldw& zOSUnpvBmkUu?jaS2GMMbz2Z>#Rls^VPAzE+tyPr1+@*jJK2YLP6;UQ)+NM>*W@Di+1cR%Pb%31D1q*#@~eLiztRp=aM?DKs;mU|{%p236ZH`lDP^=JM^M z`dj9dbL1d+xJG#3b7TFZ4Ro!O)$X%re}}>=En{0OM^7KIAIiJy?%NLx*`6Q-9n(B z54Y2tz6ID#1r*8TwvoE&IB(=eD4)Q{B@uF%A(rm#%H52!wc^q30k&9LJ>4R`)NHW7 zg~BOdSz)m%i6>5;U!q8h=lfG>hLrx~DfFTTkBG)x=JkIYsLOtTit=}>db3&1$Q5># zZFkxPGmwB9B-;2;_1{_b?GXSxY~J_JuxDH(M7Pwr6Uuuj$&gKu6-CqVQWA>na@4AO zR!iitz93`Db@Ohu4S)~1gh4=JS6LWupo{TrI$`prQt{;tcZ99CPcV&{r2>UV+C`n1 zjXg00B~zqmm{FoFqf^|Cu|%|bN`x$}q{zU@&ft}085w5}Yz>i>Fq-Jouc3dkuHL%DW1IJ!{r5VJiWfLXo?uVo);x+- zCYwrCL|Bp#@_6KKXt!qz9Kuk3U3uP#d)ktFzG>V!%%u0bOM2SR6mf7I3)bSHhkRaM z@~ht7dGnw~^%~p#PRTkFSY{SCz^DEia%2Y%5!@BTa1V~N2?qY;L)d}Z_vZYITkv(T z^YMS~r_-_tvB@PiH7!BQGKW~HDuF%rj%z4}(}Vlr#rQTGylp*STUMP4IO+1nxMi5D zXa(7#vL(qZxKI^}g(Q=B(5}B&*Tyj98=+)1?y&Fe8=)AQ*MqH_H5?W`rN}Re84*=4 z(uvyW2a+7J<9LN{9}L*fvgry=Y~7w4!-etg#aUowd*5lViZMD^(^}Cp2olVa*00)g zSKEANB(_|*D*#!@wc>pwKb{?+(bA128oF&NV^Mne;YI12HB4fr1x$R#W$ngVI|Fr` z%t*|?mrkKH3r_;BCu^qFls|#>2RhJU&R2GcM!_?`OLzss0|Y<53CA2OzXG5gD5o!9 zIi($GwD3LYxVrpC@>FejrmxJ*ssX2dZ^~?r{Os?ja4^>GU%wvAc6SmT(ZoV-7>@n| z2UN2NBQ&ZdbelwoobUnRkG1)$Nx5uUPe--HLkWD8*mcXJs0AY+nI5)mWSwFXP25So zBncJIyiN()(m3xU@}F(;$)0|M-IerrltJrnb?^B$$jwxM%q>l6^XSXEfx;5kEG;kI z#%EPsUobDYjaTVH2+045F8{eUF;WD5Gu_Fv4dzq<=A07|cB|QE9bE5Rd{6yX>>2|K zxGiv$?3YSeIj`0yZ9BUMlghFlQ_36hrM#=SWBp3>B&0@y=IR>YhX@d6O&{AKo$E_| zc}rM#FLmD4S-Bk)RDlJoPu>0FAkBmNfEUzlB4km9`CljQeCw~g>I1xxvtz_hRW}Nr z9?enpJmFaln$#(MHidk$J#>1Vc3J3FChMu?=87mWE=h5*(SpM)mX_NwT$*QHH^gN7R=**Yf%}0X^g=XW>YWsY}N4^f|H2i%{_qhU2Dzmb_qIB zvT5V-p`-dl-BO>K|I#8YLU@cJwXyh7fU9OV@;J_oKss^vi7T9DFY4|{=;l*4)1|am zlIEM?@Wo5^T*nP_ZUL&^72fmf%hy@NPqO~m97SJyXz_3&xlq1A0M=9tAf}n`5xr}- z;MWs&{O<D`4{$~d} zeSO#-Z2=javE#|<0CAsx$*}TU2e7q(X+$?Swa}Nu3O%|=+7;o{-eY;1XUSL2VKz*t z(dOCHGkV-6b%&Yfqz8ZX-l)AIi@>H=u0X|p(t+pEz?67zl&Ps>k4g;j3C>K)h&e{G z_!Tycc?P{H07-~^!SJ|y<#cm&2`YWm-kK~L@0ptiPpr{T{@P0Rz#0SJ%%>NXemQg6767qjxyobO8{olmP;a7gJ(l8}E z4_H@u@@%igb6y_<0P<9Idn|<<#pzMKMWfp%is~#9B0lw|QJ{>OyI^*|#$g~gDn+7l zO%_hr9^fZ$i~_iZ$L|Boqi`wj{m#!X!LR_eU;Y6{ktER$ox$6mb&d5~SN4%EHxqgN z%`U^MliyE}e|lrF62i0)7pHWeW z)f|huW*wlx9czyueUMS_Bb3K2lYO?G0+RFozm$|ADkWvZB(S`Q5)J$ax@9T+`a@zL z=X$I4J_2}j!jE1R0G>$zen2jEUs)K|9-B}XK-@0r zyV?@evWF|~xKKk1cAZF2ysJH+j8LkcZ&n#OCbFRqd+n|+wzu!M(6_=Q-N(g}eJY`;%3Ho+Xw)p~6_1(8ZYH2U#38P5nGOsm#0!vH)h!IyB4b8uK#{ISrDl>>M`P ze^-ip)h3;zb@&a5ve~uKh)V4`fA{l)*ghjWV6@g~!OectIikRCZG^$XyU4oxB~gUQ zg9gK}LcaWDSduX#n!?dxfQviQKNGS-)@VDW^PTXvy??(T!}+OkpYXg+sh)Ims*sgj zXE0$-(Il=PhNfg7&;O-)6)~I_a=8!loGge)`I}WnOQlJA$km2@eh_@27*DoH-_ue& z<~3-WTf-C7t02R6WAeNEiDf z3Tb|yN<%dMQIR(t=>NEbLl8FEA)Jk>fT@$adys(M2y~EISArxmCcJ)DV!X ztquF~P=4ZtHg{s(}%CsdwD2;NKFb-426* z2@%V?)nv#53lCK2Z`@&@epQzeAMGC~Tt;o0m2G8%Odz1D#QfKVz( zd;w>OLBEwGI)9q`s0BbyWbZl^DXKuLkt%iVq47*APAD%i!s z&c!0ON&$pVTbB;=K59HIbEH;l+@V$rV~D8Gc;`r2&}li`Mu~{Xn*Yz4T+?b^mi|-q z)jrY%4*ZsSsdD}jeDO5ISyp6i>4*3zE2tV{TWc_G=lN1+Rc?uLkR^X-2w_%9?;_WM z3Qa_8t`X*Q9nIXU-k(ayqFzBd+=HtrQ&i|@Bb-1~t1m4MIIEpOZnVaF^-HZHZUGlQ zZF)a;3mE=Lrm%T~ilO9HE||Jn!ZfdJv@71;9998to17SvLWYmDWu`$7jBCn+^l~{n zgmx%YtU2k&bQ#P3sAZ!QWkpX|!H3tajh#*zegUXD+~uVDg&TJ0)As)l-dXqNd9?c* zJ3^-lF3A6Msg@-l9JqP641s%UsHhD`ua5w%Tkr5zs(xs>RFUm$Ph-_AkZwrVgv|nf<6ZJH2isHBKXUUmOMZVxEB8)ib(JyY85PL;>gB z{_yf|``Z%$ECC8*)!i#(!$6t^^rRx6jZVJJt-;#p3DweNn>l>!HtvzW5qx$FU7`S+ zOF}e%Nu&Hnl4-i^*;3{aHCnEk-PA97PLtE0gCs|tU$Ne0bt0U_B3__u7rlt)N<44y zBAlMkc&9DQ=e-GM!^u_12K&yI>EGkm+?GxKqO^{>Ri6c~FXn-(1DSMq;6Mj<$Q?r1 zl;@YfkVDi$rgca^Ep3^zgKe^Y5?aI;2eu4BHHj&V(){BR-Zz7M+tZ`Ou8=_PaE|vg z1Mfz@FTO}DR|H$K=X7+3`dtIZ^J}e9+VCntIs0=Dv*q!64d1!`N~f5? zzh(2!J&>s31FDPsqpd`Lu5@=$N$Olvh_MnD&?m+t-9zT45VK8j>lZ^n;rq^O?IO+B zbIn$Gx9~G=@I6NK7>Y@ktmbr}8VeNZtY(fWiyhn0AB2>r5z$A<)b|RRm7fMDLRTop z#ucW2Vz9r9SJ4^tWY|0OK1}K^EhH|OB?;d^nF244LDSDHI0mn5y_ifw91h4Bkb9!! zyDt8S=H01sc}5$+vC|cOGQe9*xlt3%HwSWa%Ux_#^oWeh=m*!TN`~-)v)Yu0k95vN zZ0l-wm_{#&ffhkV-qBzkp51(F`rVy3UCa510(`In+Kr#2+!`fWD@IO zf>eLlur|2fsH=AVGpgTH1%$QEZFC8xZG|D7DoE`EHb|nxVGJf0mzzop)`Tn zD(drR*VOWG9yoDfH!tee9qn9(}H(s zvS7BfS6OfMNye`P{^j?bRxdEIhl42=Y}3|2`Ye~S83KM#)YX8L8n4u|GB zw07U93y@+1vV-cO-I|(ZDIuEkYf(SL$Sw*#bcdaxqu-y}IfEv|&o<9)Jd(JZR{-k( z{}_c#3GP$vQ;W8Kqr);ZqZ%>&^R>=h)0*xI@UZk#wS3r?InAirL}hJBj{LtQ`0lepF{ z^&NPVO0tLzmTwdR!o0t@0m3&8k0yp#GIhq$x7FrT9a?&iCWAhB5Qqv|Yv*Qqav+zQ z0Xt=NFuaIS!1^T*7~36H3XGD4jUY8Q5{N@9)bR>w5}{Xr$8*~Aqd_%&PW$`dhwcI3 zPA(#IES$S#;Fb;cYPnu+t&I};3C~4wJ-+Fvz!fz=`y~Z4|I%gB{VhP^E$Sj<019<; z^kYn1{n3SurIuz!OBpQp#FYTH+g3;$xgYhKoOu+>qy1kP&LYYA?NFJhAviU^O-rO`lx%!+L~q^g%d=VI=)zMs z-~8v;%8SlpeZ{pNYOjnH56cA6Q{MxcvkvpWn+f;WjxXt6)7?_o0%sx-NMq`6Gpwdm#kKnFm8)xR#Ks6iVMV zYG(jeyM0a*rkB1u_R{)_ydNUG>`6U3lg}8A$_VW}vn!;nnBSji(7GB-VDDXne-4aZ z?hYf*-jD$Hd^NF|zGGO4^yAH_7%@L=2C zs;9E7kr|vQOAb0Mq_) z%>^~Q)JW$0RA{wkewFURl(2bD7g1um*02O@rXn2ShI#aH)9EqH-PjR8VN1eC!YrWg z(!?J-k}v0D7ohKQizV)aj_O=vQNQqET>7C~)hJrp0|!EsK$No(jyORpajEX#8_iCP zhv6Q6QJ2I=eY(Y)0ghT_$e~9*@U}$#pU&E0iql6u3K*s)*3vi z;#BW^SBogpPkB+T^S-B!hPpRyoE}{EOK&2n>APu^D1KExL&Z9eI&GK_8p^d}2#|XG zh=gh7&u5&EQa$X9*Dgc$d}IvY3=(E+HJ{F z)JW9+E(K|?@Tu$8y!|9&_;q)>RG&S>uuzKhU9tLC)d;=}ueiN=A$#`$tQOb9DU;K$ z;5BCZh&3d*OY6sdI(Xuw5;Gh;v~v0SyA~^Np9>9Zc!?DuEN_L8)X`@Cg-E2#G?=Y=vf_^6Bf%@t zE2kykPPbr&4|a`(>yqKM6!?AAjwVgI^A|sk1?k1B@A)N0c#Wr=kjUPAg=NCrK~yzo zR7WtBzh)St$8M_c{Int#?egiPEDBb0xpkh(GA*hN(>5B~z}w&C=bN^WO&gJ}xZ)0_ zPp$kaD^H(iPq86#Qf+~JZsmRCKN3cc(%s43E2DVidCh%uExO}EYFEws7n?t^8w=kN zHXbC}F!hZq&=D&}4}2qA$`k)Fg5l9xZ8L3eVm`OL&aQZQno0tRLw^4#?G9@_IHr$+J1o{lZXFmVYu%<}DVQ)s;k)dh5=h|}zlhULS#?KFvL1^E zU!i>G&0fFCiBE6)>n!uA;ARoW{G{kyI=%_wV(`4z1tsu>0A`BhQ<~u`-N|<=g}d-e zT@YJ}dEpB*>CZ2wNT>jZPFtBQ3m4*+$3Lg?oG|W6MnmW%)l5W7hoc{LQqOz zrl`^qh~_Gl_#(3{9by9F9Sc)%(QBgg?S9>SG5Oa13p}LV{be3xDq*^6{J=ix@sc>R z33olj;ql+bB%kZr!OQ-RZh`dB0OmVARCQD$Nr&RjCP+H-HZ;y=kWC6VmTOsPfgXv{hB z+j;(McRxMdx7(03s4jR9BRbISR8LeD7ACzxT&#?`q!%2c_aSF#gz0Y<5AQ?Y4Q(8y zcb@hRgD1&qI^^#?!sorrsdkv3M&FY!dvvtVP^MzW1~-E#UqwQlo~uAUrZ8co9<4gf ze5uV4e!R?_@X<)MlZB)JgilWQi}m;jQd*ZAjYGrn>t$^E*BI_l2^k?wFBF+j5-A;+ zbBFnU-d&o4Py1n0G6o8N9w*b`J@<3|zrsBQ`tcZ`xr?@FI zXZzC*$Z$F33_eaJS!}`YU&bFtd_8L-KHk^OT+{ONSJ3`bt`lcaW6Qi&Iq7vl!uXgo zi~l-J_2$_Bsg$Z@d(V^k+2ygLYdGpApHpT}5lRJodavJ^9HYWf(3D8B%UBD_=Fb~u zL>eiPzosOG)>wTbaJZqT)VqIc;>Lcz{MZ!5;+AD~sfz2v@ks6VS|L34k$pamM>%iU z?@oHBuJ`a=B{eMaZnKDq9K>&dKon-AlpG;^`c*bWc+pWs4oN^mE8F6g@fIM3q*SR# zM1?x;T(n^dQ{GZzv=YF%83*g_GIhiC;xPBl&ye<9EWwQnXFov`>YM2&rcV7t<`T z<^BN5WFl$kRZu7@Enn)(yyQ&3n)5GO8X*%&FOh`LjE|uuCS|yEqjdosTk~;885ci9 zzmA6$xQ~{Djo3%O09V*gOv|W4CY;JrE77M?;fKx9x%m~oshi-rqI^1=W~|>Uf5%<- z)Gy0>vGCcmDl8GS9&T#mLBUvE%-x;Vx&N)b>X& zTzIop?JB(Y8|L$gqrmE0GTnBZ)ka>OfRYqaibgG_V~j(9xxzHdJKqfz=-2CAxvUj& zRC7xcF+5HURG`Eqri=ekkd7z9@^AImp>uklb*k&#RA6A02fd%3 z%IuJ1?|R79m(Ut6{f?V6EL`v6qybno0P7V^52gcD)<^;9N}0Zt%Wn*sB(;!i2QI;* zCDjgw0+&o_cC(=(hB%c6xzs+YI= z47)B~x|bKts0rIAivV^ERKtLl;~!bSz`NfZJyl^_uTAooE_vP?Ve2OK=JMco^bkO4 zAh&wEkas|6l7uiSVRn6fpdt+8d()#zjK4qLy66O7zSVjJBy>#^->oDD18A<6oVFyt zW?^8#Hwf-~9I-KxlfYD}EDX`_y#ZoN#;qSdwV~C^hSYM|flT%qy-($e`+9tKd1_JP zA)WyJMIxe(Ja8jhthMFtc$g*6A||cSq7`zfQdT?H=3Pv4tpD0Ihn##Hwwnp{fRj;Z z%@Zy@k4U<;>TCzOFSs$%fn(~j=@C*11POJ=@9B&i&*M9tVh#;_)U!8r&(LR%N|(5< z`Dg@h2)=kIk-|T)Rbe z8gRNi>iZJ=7fp%@C-}#1R&Lcz<{KCUn%3xglzs#Bs&Scq{PRhQ-yBK0)`DW=_QW1- zEF1R|bvQAU_KZBN`)GsmNb791)9}w)T_?Lqy(6)ot`G&f2Y_oGLx_kF%&72*-G1HL zgNI-rA_mpnW6gaPVYv58dKrh#;H>@}N2<){iPCGY2-&MnK*bwb%yhEttyg6R=g*2E zL$Pp&=t%Hn+2urHnPD=Gg9gL{atP;4CtKX)7OBtGM1Sd5F{MJxW?$W9tEzF=hS}Bw zPc5`5a1%&&(rRRKiQ=-=dwkh>3jApxTGPQ|YW;@P=r%w-FjVeBK@MrY8Xj2RuJLpL z3i&-=+Ml$d1$h;CImoKhm}i)I!nqk2B;v!WDb>?<^ZPyTLP0n*YuNDb?NQm)_B|LvV14w)vO z;I>t){nC3XpV<|Htp3!gy#H&4ktV>5Xg^bF*^xH>NZ}0h)8carGoQLzl$~bgbO<$G zIVi@3b_;_LhGx2PwbBL!RzyZNu0L{LOC-9&@+ux=adtmg_$o{Z;tCEikrXi6=@I1o zr?WC#Q#E_>XRefE4=r;qg` z4cDX-bpb}1=MBU-w~)*^7jS#S4Hp49rjfEA^ve_<4nC~a3>$K`82&QZai%6?-KHJC zpB8=gCC%WCf}SfR2RC7!2ZzI*a9{uoh>bJhYw`h5$9HH+lO{h*6?lX4x#tAOQJg9G!_giJ> z{kDlF+0{IX)6c#_^FRJ9kY5;$BK)XJ23uvp>*K7a<~%>%#QojGN%x5{vGPmY{8N-S zk&oqhdxeNT^FS$=?tC!Acy!?-o-8Xwk7cb`4);QaXY^BKPzH@fA;3@prO8~WMX zaLzDSKsVo5Gy?AE&ATbs(M!^FjYs7(hoK+#} z@bnRsZ_|^-HJk^v+}(cO6pN}SgwFfJ(~r|xxOz$zu5v;sosX+`)d$yEeiS2LfjP(V z-j&0O0`gR@bc50iO}w7J;wHT3pI>lIr$ReF>TjU-cb-0MI>roMH)+PxiQ^hhp~k0p z2#ubj=>hc^`a(H?o-SPFMu$3<|6PY2wXe`Fm+C-lv2;yvPj6f#qv| z`<-sR?Or|{i1&Ou{rkBC&ZI%-_7xr&rd=}XI+3e%KOBGE<-GQ44)=NT=@;pqX^*;c za46N)SB>Po2}kfZo&((P1h_!q*R>|zufF-7MqGHFtlz*Wu0hiiC494Hjp)wXI9_&d zzWqMkb@xMb&s{h2{H~77bFn|TakI`&x|oTxRE59QjJ{$x&mSAhVPO)OfAF1R-Usvf z(Aaxu!@vK!<%seuT%-{F?>%!sEsTnf@5JA1*kAi!g=hEgDTZy3`R=@r=<0E|x_r%= za$0Y{4kSK|ZvlTZe28woag3~);oAs|L2wZ7r+jGA*m1Ye?p^2@nAwWwfc^KU>#rLn z2U6>IA%sO4O}zbH4r0npBH|yxqsX!4M&gK{1G@320oM#lJSyJ@{dCPxhQO?hf4_hB zJ^GISQt1Mv%KDP|kwy>)uB`bD!eBPlM!$X?^y+!Ci>_F?$~}DIop(RvJyczV1_R`H zTwPvEfApcd8+MfA3QH|qcaqtboyxprZ zH~Znd?dtIkL~9ot-X)I^wHjldi2rHdLz>K zSh;u!o9c8Uy6`(r1E=-ZyfwMgWQ8ye4PgBG$n>Y=FBKx{j~0K;8eOm z?wUyT>ec0s(fnp&2P31$pLm9iVs4LVEPG77qiEv98~Gz^6)_mn0q17kz=)gm1`i$~ zlS>ik{1o90DT^0iM-{`AwuH0G-7_;l`{9PSW= z8p!_|z-D07?`c!-bPa(+Bj5}wR~^2@giJ{10$LubB2nXCO{yA74J` zHeMLUjSg^Fn4R_P3~GPE$#SbM{BUDem(C|}mB7ZIQ|O7A&%)bvcn7M}Te%bmtX%0p z*Z6ns+TQ{o;%!kFZ?#~$wn|T z*7q@PLA&MV>uB_t>p*rL#sp9EtZu6%W zmt8|n`X=A|n3<6=?mBA$OY=8-?mVKo%w^I2I6i3l<%>-0R+0Z8va@jz*{Qxfx&#vn z=>Klm#J9T4rZan=1eCgUZYP^D^#&10k@y{X5~qlE;$`|lym|240X^M%1UdNl0|P%D z`_|=+n|*tCrzf9@F+sQU4 zVSKC(bWdj{W7w1l<3x4X%m{;wefKFV>4%=Mne^t{?xWxMHb$>PsA38VR1Ii;(uJ$s zAneNZyH2g@LC+`{+T{<+S4E-Z&+Xo~!-w?a54_$A@P>ak(YN0%=L5X< zr5b!Q8~Oo+r*a}nem3LQAJ#Q)Kh!<|wC zMBK}9W8A_UX50)_|Z)-3@!b}zo_H5H5MQ1;1KSr zF_-a8-D_#~8*jSd@Bl+w+xQpbHhd67dxRT);RS3YcQ9RY*;Rt4`sdVBy161T&enM0 zS%q)BTD<53elKFfi`RaUCphpvaNlHVa%|g(1MOx#Gb3R9i?J*=qaVOGal>KFY0m4< zQ|q>!V;&ZGJR2&jgI+aW_+ec978@A1Xnmq=?0|fnkPN4~$Fut3c@yhaU9)4G(5*LL zuc(Z7vDUVe$MQ9}jFC6w`_-49rbbQM$Zh9|#^DN&@{p79zF!;;_fdy2yUrEhhdADv z^AgXK%y>@u(hku{=8Ci9$tli>4y0Y>z+(uFPm;nvnlumUli^S-cOg<~`oZY&+Vo^`4d+44^+m%5nn&xmHs7uZ ztx3AL%#lvH9dqtHHe71Ur`p$`yC+>IV_N%wsyCR&R1+^glg|Wkx2|zGB*aOFNf-2H ztsuaIJoL42^zSx>J z@WHqr{BNPAgp-1>COmE`#dutC#4Cyry$J>nd5~Y9zh4fv#L?H> zDsz65S_vvY>Bha7%#$5oB;Dpn7T0hJH73PlL#YB@jc@|S<`3p9y?kETn|MB7`ZXVh z@&g-toA0JW2lu0^u9@hoBg`kPh9AFn{x0P{9>%Haur>sP-hKHXC~a>|8rS1GS%@B; z@tniv^dSybf!iwn;E+at9mkv!^W4JD_!&I*xCoo2al7rxAA{dCu`c5`{x$D6dtJu& zelyvW8&!wO`wg*~_{{sQ6u;lh*m)mb+d|i}k>CE{+KW5q=^5yS8PUPEL zajP%-599Vw;X*~^o#`L*{mXM;+~NuyoBo9Ol>F{|lk9f-Z1FMwJ*SpyyLFAn8x2c#T?Y53$GmhVxI5&ja^ZWn7eBkqMsN&Y8dyKlfnc)Tg&PNPM5}MX z@zh`*Pu;@C0c$ayVp-Fisih@59#Z z+uVNL)d8nN`+B4g>GY>h^JJLq3S{L+=blCjEeyirQ7_Oifg(QO5^UYEb`3eav-;xbu?% zFmz|CH;;L&_d!l^TlpG~&NKY-%a$!|v1@ufZ`3{+nK|innkBVmW2-xE9T(L|4BsfR zH~5~(H?e{3gFL=}mW33ek3ad0Mhrhsgtuj*FO2oK>!nJr9&np)Azst#;L4u+);zZ# z=+Kt;3r!ds)nE?uWW;;#l*oNS+K<$zb_joGG4t0N)eogNUw_J-QzBu7*wV-3SCT2; zU7vsUA!m)q_~|d^gD^~k0o#D{M#^_uuYcu(!JK&T?MvfVxVZPdh52?eUd_B)N-=|N*RhTDt&pdS>RdBvh`NQI%smYUW;JG5^ z(4O<>U(*>sH2sywpJH4EYelr7P4}{ur%$)PV_OwsALEQirUdpl;(PTUfBYqDR=8MA_kH0;-q&A# zf?BpQ-#x!v_AMWVyn=54Ei4D-nKf)Kb1v8AD^_Wec(2>%?sGg3=#L(LU{c_lIb>qb z*6(a*f=l!CmSglk6K)tI`zNtSC6=pQtAl~Qg zo$I!YXcWsjOh}EP0T=%kxHl93k%sF`H^i-h&(&5i1? zIWC%;{n{+vP*O93gb8NTCXMKIzRB;ZvDed+PwB_pAZt#;2c>z~HCJCry?dYLdQ6yb zyKDRl&sw!=QW@t4KWxHm(Zcr@WI8^o;^w>e-g%t{3>-oaK5(}?!Bn9bm<>AbY}shn z|LpVC+l5zNaS06_a-REP*Cp@+;;w(p(e%OlbLgyogJ|KWpSfh{PEV{qsB&d`=dD+$ zCf{)4cQ81J$F*fEIV zZz5XZCx_3W2J*EAupxWsuuDQC;7lw<9lOMY4#B0T7|$b}By0Su$<1ve;GUtjrKuXvjE3jQ!(DFY14!U-L-5k2%iLSh zV#!RUKW{DPZz8-w0g(UYADnMgTCvKPZnMzr<-><_{;(K{wXLXh$+r-F`Pc9ax%)#8 zXu?OqP0$EBd%F&s+df+GncR+v+gLDW9C6_Q-o$s88%`q85QxwhZ)yI>GwR|&Vw?*W z+z2}Dp(kl0Z%Qo8MuM$dG^Spsb(PR9n>Un&(sw^t5KOlb8JQpTci^QX&*M#SWkuE& zzWr(Pz0>(HfZqkjtxO|_52Rw;cFLFEm+IF$oR%*8PU69a^PZ=75kCC9|KUP<`PH{* z2QQYE<;|KsdvxJVboGROF5l97^B?!o+O>a(_VIyeB8%G^kOp7}R?KBA%d8V*n9B3V0m0 zqK4ci9q>#A;>!}+n$FSuqD_4@Z3g~YUty?*&Bc-R^)IQft_)us*r5O0?|;$6JML#g zx1G#VNDOF)$jwECrJkP0x5wPdbnQt_?shzH_^vJDP)DcFm_>^}hmr9fzOAexU2yIh zd^pwtviX0^M8h z!=`w1IE2{L;x^lBuf9U+U-y%{(&($M7rAa^DSY_uyC0};|3R|J zALHU@dA$<{zreu=79U#2-#C$GJpPpUT@0gb{!@DQle*F~&%GcxeqdOO8-n3r(A@Q; zUhYl3Fx;Ix`voy-#;wB`clJ7Cfbd59!H3lI`H;LXzx+zVz#zJJ&mN)|%mI3wauPlK z=;QR|vahLrBQtOK;oHUThhp`!WnVDJixW5eo_xv~GLHZs%5K5@w`Gojb-+_t|1)@g zk!T!toQmgkysvT;F8JhA8hOcRnJ?fVpNmHfV`HyoLdHj59O{SD?{m#bpQ-($#u^J( ze}y)``ll0WoD8MA$GD&xk0WaM2&s9TJ@-8)l4SGarK39Ec&ImsdcMIY++1PyuW)?k zRpv3cI6t`0AltYZx8l}2ILyJ!xRsAiW`}p=fPUs?V6>M}7hk~R)|-TepH?j!yYJCY z`Na7dGr6yDvX6~iSFvFs-heP#D#6B__%B|ys3L6w-?&salp+RZ_YFGRSw{#-wTgst@DQL z=tc`Bow&6Sx5KK#I27v=HF?vX--zsEo_nt54J#54PiO)CatsM2@b$?-+no3Q0^UsZj%K8ObnhnI{Ucbm|SSQCFGZl~Cs35WSE4hq`6dzUm*%(HLgTT`$Bvj{)qhDzLY ziF9c*#9CfjaFwgjsiAIs>aQ@(bhHqZQ>^%vE{bG6DN)uFZhtoI+0~fH@%WSRC%$pw zJ*h*Q9xYgda}*+0(;EesggW((!9$Nc%{S|e=1pnEc)g@F^*i%axf$5Wws7kYLYe&W z1IGiyL%;FwU3Wh$H@ylI)BQKL9?3WAO8t}cIdW7mL(CVaJ@7cM;r=Y|jKPEY$$@WR zQR7Rrr6-?$nRanKJ%SHI8GJr&pf~ee7s`hsRW`MfLFyx({i6FcM`hi1t}ZYk_>KDy$lZtgkI|P6rLxXJ1n56LgaNR`TS#v3Mtb0O?$}9Vue*)5ZQU+()GXX; zj@ygjTJVibif*tCSkHc<%kJGy zpvNAc<;L6hj8lDm<_@~`_NijTjdYdZer(JY+Gm!fi-r#5dH2I^yc)j^%kg|j9SoSI zWjOiN{KgAw^BPdE)4HiR`ryM)=;c@5q@BBX&4lM4J$VhXv9rc^&e^?rv*uJ<`|BTW zd1_gT0M^HQAAC%&bAO687Z}){)b)5d^Z<0==ueR!+4tA$t_C*4e3JJDU6E`?TNw`|xei-kyiI`7jN% z4^+-yeqKvgj=fpZap&#hq#YpME3aZd(K92y^zm|Z&_6}E%9R_IplH=g1Y{4U+x&^+ zDkp?SpNZ&!i-6g^aLM8tP9e$_=}>&{fwAkm-QJ`z`s%;Iz3{2RH6E3hH@N*i$Q)eD zC8!@SeKtK=T*E2U_!JMJ|2?JSSZxpK!%ctb$xk7!8It#)Cs*c4=pR!=cjYTuvUDm(c7&fs=>8(G-um%^MSL6X z0^Z}h4<8rgmo3kL>aeGEoh1N!%-2Opm4tKTO6Q}|LKHGBg< z4*E0gk&S9@zQ(1&Q0}Ey=Fl_GzCyM6yJ?@E-KkyMmaah<+Aj`X`hNLJH@rV@dZ-Zm z@EsIs#Jo|pfOVf)Pd(@ws$ne)`&qvF`g>~HxITZoIY{Kf`0kMhCsCUYr_(N;Pr<1S zL4dO^w7shzdPCE}s83`#rRbU>(z$B&&$6a<1g|IPVUvgQoN4l;@icV!7!AU358ZY9 z4FTidA6NZMxP2LhevNp(2-uL9{{vdH0+b#=pp|-InuC z1GyV>6KV|$>I8))iX;2;3;?B z=zh}${}*3;P3I4}%roL=zgs6gG=AI_ZhY1G+X>zaenYpeU5_htTVh*J-4nX4grMQTU(~5i*!EC;ic(wJ__EFTeT%opO^U{OaA=y4{uk& z&ALlx5dCT0EfFhS4bJr_+tw3JJBlw!;U#Uq>-z?|M#8v(c;9MlB)r;ieDN4j>#^n; zw-UpMch7ErX!CqT56^Gq+uk#t`n+Posqeci@8jDPGqB?%A{ZFM6)C|6^t^og-023- z`=;GRjhYLKn&46nByJ5q> z++qHY7B2iu41e3VYa@ChYqU`z-YJO~0Qc+L$CZP8_`|~RqGzA8Y2nB3%DWfu#Rm23@y4d5awy9VJ|PqD z#(F&V@H-f4=qvT+CUhDdd|T_Gn92FrTO8Vn|S zvq6NO%#TytV0>^49~J^bT)4B&>`i_HV7%wmT{>#4Yy1m8=)d293+S_j^W@ew<@3YA z=MNOU!4EeSPq_IGKbf9?;T7KeR-gKw*-OR}F!~;U>_YQGfQs5379nXRV7qasYh=ha^JYy}Xg_=JA(v8!JNP`F5XEPCk)89=#=d9-LJ< zFO)C-rcLV0Ew~`!_Mki2;BLouUPa=m=&t{E)BAj52Tq-B*0jEa$HvKbd9$+Yh4jhE z4lhxv1XVlaV3C1wJ8mA`yonbOG38}BtA1L;27%MXzz|HhJ!R>a-$^{3Pi*ThDXv<* zhMs$YSE8J7n>KHy>5soaHQ8to`NrI(b&DqS>cFdry^a|rQ{u>m4Gx-UY=*M80~zcX)Ch8Vb2tAFA8I$M(=w?6gT=|Ln=PHLS0ffov=XRVbr*XH$s<`+q8`@ z(cRN#i1Dw|zxj3rZ#sTnjDW#oHZFMK>m7Zh%)>$Z>(770KwHy=bVJT_vtFmsY?KU# z4?^aK8x-RnX}4>*UFnDE<2$sXIlPHh;|FZh>R1VjpV@Qf@vj%Z@R<@5dCB2MMVel& zMf}gC&&ads@#8EebScunW?n$n@!6q=R&}LbG@Sc+R62H_Fa46P8-I&STep5aU3}?i zYSOrY+@y;&qkd=gamSpeoq7^)NZ>CY>|o@2Qup9T!Re398f0&m18uKNvuPwR0q zyk&#PX}$Wp#=r2}vSkZ(KlKdy)YV~w5Nu#U90Sf7D$gndWrwxAu3b8c*9j-IlQFo`a|&}h-uK7Ngh+^UDuh8aK|8zl(kD)}d zM~r{{20=cKq#tp+s>(M)lFg3^eK>#M!e3VIqxp-xO#m9525@nHaGyc6aUy%%x{MFh zQHQY$4hDfCAvSd22Ejx4`Ty))2jGs?_rKbEDVZTNN>-8VkrAbVBoP`CN=s9_Uwexp zd#^}DMue=2wCvf?KuRgf|9sB5=RWs&-uM0P#s7QwzV|uzjC<~Vp65RIxz9cKVE1tV zoVgU{%v}_eMa3Xe@O;{Y2OHzy>370$$D6;&`H>-HlWOqNY@)kj-GOHl9lC6L;Bd?4 z@F+b6_}X}Q^QC#3vX2Zx8Hs@k3abeElj$P$ISR%RK9E}^A#=bl=?w~*oAxOMJj*G z*6*A=r~qjXIr4v{9z?hG0d3Blh2~w_&`R_g+E{jlS^32`=9wpZ^B5iXCL)XCIqc-w z237(UlrP5tofb-vBeClI;>&NySvgp2fM@8aF_YP`jupsd66oPfPj5!!&R%Ob*T*X_ zi|N`EamVmCR4kZ= zl89>B#Ht_NYdcsm?$cLZe}|$hJxKGIM{*uEeBzBJq-gUEMctS=Yrb>x#pYGE+N``8 zIdv+GpETX9`pSF0(c(o!BgQlB3_CAVnC742J>O=3v@o!jj7H?vdE|g9&o^?er7=Gi z9!BefvZepn;a=O(f56fHnWuX5dj{s?%AHf1BYHr-OzE>-d!nv?!TFbfUT+QNlHUJd z4!zgQdj;l^w7#I%s!1!WEWo)}HuaK>Jv68F*i)~V7oUBI`-}?bmKA%g(LtYifcnh0 z+4+}yUFuIU@Auq`{S7t*fFpL%qV&E*a!Q8^ln-2=FTioW6|lfQ;HTb?^ri;K93M9# zDa4$~c$za|dCL5Q=GReQM2tG^v{SfUYtr}su@s#~%YkVMv*#}04FXkuDLF}hvFaQ1 zzbEf={VCoJ_fUW8w zjGt=SQe;g5Qt#L_O3z-LOQ(xM?My4Ka+*fJE| zyhZ70#Z@jhmG(|eJILh}$fXrmIZfrgEA#JHsw==IZh({FxR#T_sT!^WGn?vrgR59N z=}un){b{G$=6Ci*a6#P|HK?whpk(-+yyc7F7R|iV9w|?)j#M|<@^zCQwYNZle0rGW zX?B*Kmdc{-q`fg_kcowi!lU-c`K5CBxuxYDpmBI1)Ai=-9XmGl!M&;WC?TFaHb+(` zH{55}Tz8AvxM>^9<0WO}=!s^@f@%EC6?z)huSJe|}G>tv4Xsw7RM}ZMryu z^L_iSr;Vu;#*CZ7R-r$yMc*#fF&LY=H6~{;aTcTZhb24BVu~=_wslL>`yTOt*}3yK z)2LZn)-NwldbO)|*&ObUyC30A>RjK%5N@56-{Mujs&zcOspK-djtVkwaxI zGS(Yolc>j^dYQ>+NguR73g?0&oD-Gvbs&QBZ9N|d=DvwIpx^T}-&c{@5CzzmuYO@= zw`mg?-h6AQ=|_J)y!6~7rhdJeewMXJ?aG=|zY_=T5iU2ibnxc{$S&t!)EzBgM=fbe zb3*&2)&@BL4(R_Pi{Y5!V45yB_sTFQU^uNRd`WZ0_}=ORBL3dc;Tp~=Q@R9i9>DW` zJ`){d^UVshVVVNIfn49IyP__=_x{ICL0Z3pwIP7(uWQ8)z>XmU53u$WeTc&D-M^b# zy4@4%Z$LwYT||H{LhIfD*jQWV@HivRPs^)1(1!IthA8jTGXZm!J#Op5`B-;w0d4k- z4GI*>mk;cjMst5yJL&^A9(=R^VAHLuJX5f7XLbYn;9rm5Zg%}m8}Kx_rttCc1Cck zn)h#xb^ZnKSaL19cXuFl?zI$r=4rtfzeEne_tP<*=ELX`J+B=({705} z>zk_N<^HCl1>m52$L-zB?LBX0EpP~a`IWu~QG!zgz7EJ`r}J-%=1oks3#&Nu@v@B% zlo;IT!bS|*o_+3Rb{=(%bO2|N#+L*iO?%yWznM6RdRhx`#98_IB3^h4U2l?;Yu$Ry zl$>sPrzmpJ;reUMU3Uh9!#DcAZQkrRFv1l^@xig>6>ZJxHCUJ-&buE>qlML8)ZGmm zH<=cfwQ&(ziqYb_nKRzykJON9(Y$HM0l58jZCo2-a9Hi4`nZcU^0(R@vnj+=2bVPp5M*E1iYgR*J# z6V%U`)wI(tB7aBA#l-D)in5yDMqUA3<)ILD1|YH|7KTRU!MvOn$2MzHm-!HV6_J#E z`c0)u@2l%7cE0p+jTZCEoiA3d!%74D{YlA4-aNva< zSXc}PTj<;b{9O zZ8WD&&%;KHj^X+Avz5*P81qC5g~>@m$D`{vY;+Nim5vk+wupF)&3nL`KmVEJ2)Ny6 z;p3HGP{gu0Fy*~zMje35kRxwepmyD+Ns5ggGtoH!V}33pAwA>$s2!83E#kpqdiPNW zU_=Af=*{tXGdxc_;S5)tkqV{1j?qZAeiChLLQH&Cti28=3iPBMw|Z6oL7c8#zsaoYJ6PH&1@%Cq1??8J z1EK-@`LTR*ALw0TuM)4^r0I0gk#&G*D!G0>9L2D}5o1X>$BJE|-{)gZq}S0U!z!eF z!UH=!KIUL%QWx;$rEB_dJ9o4Fr(fwA^pntuU-iJIZQuRCj%?sPl{SXL`y}WBl79cg zPc&8~=RI3*mH&&rtE^eOfyUJGjI30ty!rauwW6OzMIO;xw$gJqDsKx3l_g?QKMX!?HU`lLr^jM&I3@lsXk05YU=aG3h75vNdx8J=^f-fXsnKJ_P-T6Py~~X2|0l>ANxJyp@jknd7X(Ef$A=PJBvJW!Mm5#HdLIkwyS; zKE|^j;{eRl1GZCCDd{s&AF&v}aE9IX-H+5~7GhpRrbV9#(0VC9XX3|OtAd5=dhID+ zYEpmBWaqUJ*<~JZacq6*16TNZE5+j})&B|29{0d%R3o(5GI;Q!y~uD;a9Xm5wpaR`sn z(~7Iyp_Ae^VJ9z2d7(ojjndPHb2sIcmsUJDzhQe+Uz8q;i(F#<)Y7B;ISZ#ZG%`rL zaUG{+Jg(*BrqbSiJK#RU^@j7{=PMqiryZxO-NA_`$i|;8BMT3*v+T4~6X!i^cT7(4Fj{kZvf&LoyoW?w(_gzZfa&JB}LpBvv3 zo0J>oPdVMnX@xQ&Ez`Q5Sv_`clA5#A`4@)ql||Quwp+?llH>LdUzbT%GzSr}Idv2e z4H!4oCO&I(1-9yW4(jLN}7$2bRI+>`CwwW28J!(;f`wrLs4>qF6T6Tyow_cKpF z`5==sGxB59rn`GT7UOWMHg>)3UNd!aP_OnB?fAY2pHgxt#doo&^RF_D7&*Z?03)v< z^`X84u)ii1(SXqwQFv$YD1*P82DptJ;HJX!Q$hRH5FVi`EjLYd3v6F$%u9u6Q%JgP z&ZlLM$H=s1jGn`fHl}DD`P;Ep9Ebn@Er!kXr@aJ@K1uR__%xP|oc()gGtoW9Ao?%h z%=0A0@82|4{1bg9$Y=Aabd7ZYu5ow3zjr5XwCR;o?e>6%=kbI#9v(UT=phQIG($4s z0DV?%W46qXFy$Z=zb?{Esw zJoO-dqfi{_>9l@lN z#L1PB8akX#ixj~J)A)dwzD`}bnYnXgd&J9H1kSv3=FAH@|3VuU9dzq{rx`S`FDoxz ztSCDc!$}2@UU^m$`9Wlj#~yva<-&;-&dbTcxbc&m^DlVe4Ew-CkDE~==p&ki2OfOP zIsYPS)8;L-+dAzi-Lw&Nlqp-`Y>XflDQJD_*Iv!N^^Qg{_U-$(OqddX+-7v*;8WLk1 zHu7oc0p4ZGj z-+xbzf(ZZ%;t~PIW(r+z>8ytdQ=dN}Qw9<+4h87Tt=zvJZp%`-MFak1=+FcRZ1w63K4ZxYaoT zvqf@(MaIr?p<|yEt5G?~v42W2C z487N$Odo{LB&U5uBm`XAw4T!i2WP#9B~vSwwp84ipeeq5l%5vc8J~>@uTZ2|-H0Z& zX58MRtLaI9IRTEN9m^Fk@1vlOp%|ORb6^GEa&J6`kD?0Y&Iu;)Kb$pJvqLF1qv=C) zN0|w`X-pXxQfak&zq%yXBicUQGr5oxbkTl<=1>olcOtN(kZk=fNU@6Z5)Cqiint+R)E8hIuolN zX&q>k@>7eeEr>+1oE-U*g8lpU2W0P_=1hxo`JtvDgmK9{`k1469~Z@ezLel7Rjy!> z%Pd^=9%8Ax)()7>TfR4=$4;R}M|&RN4cdwn&d-rms#cLrAdaDH?B*`*IJ)Q;G%f?5 zw!5A!&LO>WUMS@Kx(v1E9R1u$q`ZS)$%k~ zvtW_V5kZkyxpC9hnDM1hXt(rro7{L8<4k9#v)^#!V9nQpRbFu|i}O(rO9T# z(t%^MDr{;4I1*7(eR%> zeO9lnp1l*`Oet`HzSRa1uGM~>KLU=jv^vKeb2Rr!kd=9v)>cnA@tXP}JNc9om5pS|P@lOp zq{p`(a(|$II{h@3Iq^j8BV+Z3<+O+}pU%hQ`bA``z^;IQ{l?9T*f~D5r+|MR@O>Z_ z#9nm(Mt>UID`KIOA!R-d`Fy>h4<<<6^%mF&*?w=2Fi$;p56__jS9oi{Nb~1jnN!ug zL}|!2t6p2v-d6VMN*WGwI~+s`(5Y`aI>NA5cvBgOKkjBfAV+$4=HAni_1EY~l~2r7yP{dUUe zzBx}kcCW!-Qh*Jry4-RneP`TCY;>*2<>Ex}OvS&z4r9Juv(8+7%}v6uoK%lfPCn6` zMjHqnOYfet$(0a~rFUVy-mvlT(&a1Y+o}X+tc zmM?A=^?4_*X61=TTtkk-$vF?llS^$Lw}88zv>lU6Z7C|3+CsCLU!wkojabg}J>k5x zU^mgGfC~HQ8`Ouh<|*n@_=0SqKn-V|RfzMhY}qWX190U^vN-~nFpvH9H{w^}6nO{O zV6e%hvAzx-ONJc!HbEixBMCHP15s=mcQkFn2PkE~g~elceQX>ebYOEg-C!GzZ9ZTQ zQ6nXTwsg@4X3j?o%-|uT$rojv&zb{|y*|i0WJ6Pn=fK+Kbd%u7JByrZ@mJ_VV=B9j5n{oK7jE;6#ZADzas-1?_j+FHhaOhio5UX zW}bNJCFVU;f3&MSB05+;R4bg-^-^bFb^b;A$>OCP$u}um5b;B0z?a?5zpw!}5C#+q z_$aD_3*-SG{LY)SL)91N>Q?PTynfV=L=kr37>ww*8rAR8W{u6rQRBF{^vbKvnl@}u z*Sz#%uvqfFsngAaH$#Epd5hCm&bB!bOXn12Cc!`G9D>@gnwO;p53C z(MNB33B^uR#VO)403SUQ!5E50l2U;F>C+~1ltajDCr72mO|PIG!n_w^tLz7gx`KQx zc*MlR2h%5-r~mh2$l*I)9a3sbE?;g7%^N*zzJBLA2Py$0*=Mdof$BjHo@5JIYp$WjG8sYQ5QP4Y~7Yn)#od}aI}&o&UVM| zO`T?5c)o~hICY2?g=D7k>yvp+IRmwn0(z`)@o+^_#Trgf_;fHDjpF-8xQOnqlt zL{~@;z!_OVqZupir`st)4IF;~wQJWflc!9xQ8D>0)*G&%m~iKmR#${@&Ck$Oz5ygICrXdSr76&-=&KoWqPTDcze&)E zq|}T^gEBGR7t>VlTLVYYBG0>d@jn(wU(@PRGmfHlIf(H4j;k9t%TBkbwd&IpADgfU z6mSkj^Zk^Z0Kj*}WsS^9ko`BtQHE8?C$Me%<0Inqf zBA$wz+K00&^7zoRpZ>5p4x9rIKRgdP8ye@-3wcB)4VNqO(DoxaEuLqrgBK}PzH((t zK$HP?Ac9A70Ov=Q7t7^W>nqj=_04)*YLM6-t)bYhSirA2VY=2ssi{q|=pGSX_Mbf+ zdS{9@j?hz7jx{<`S?_xNm9KYC8Q+P($-%>O$jg7`8MX}4@3G@h?3Z%SDpEiSqhCb0 z#x*ViYL}#lFeHu|i0WL_d^GDh4t$<@?nV*#d(|w1an-802|?9V>LL*9#3;UaB#r&sAE3dx7cv* zP=RXn?xKKkVoJu7=bfAUb5M0=REqOAVeGKb*f*neC#OfxJ@`^P-cL?6yH7niR5l49eG&sKa%h{`M7!|9MgPJ=Q?s z+$weYlgGiqiyxUamOo0*4m@bAJp6oP=R0dE5;)A7Jj=|4$Kl}D=iSU4HBKagiO6c?(A)&qQj9`I@+mC{>!{IaHMH>O-uVd?*NapE1Y|d!C(7pucVELHk%>CCh$geQTfEUmZPcG zTk%6L)$^!n@Vv>xn@vc&r=;Hc*wFJu&#n^1_&UM!#+_exK8eH^XuvOzodA#0aXlzN zpWa~jZrFdoaB~OUyMT%`mw~A7KDeur4)pQXnj(GIyjb41eBOA+zPwLIKESsJb}=rb zRME@DJl~QZk79wHmXAfj@7d>@ZiXYzw@UPUtHkf-%RgOZMvi%xBQf#qhW_3I=|3pL zbs$6s3-S4zyx=>nlq>XmdfjR;H@AD_B!(noqsUA*gIAal|rWcyFZvS!N%5yA9grJjG{@t z$aNIRi=39n`PN`Gm(K@00HUDb-Fc;j=VfX2oCVNQ(uQ)qh4tVb95L!$b7SYWs?H$7 z*tZm++qVbx^~-n6bdZP-&WpZW>T%nUR*xIcXE-k91p1T4l}}Ucm=r&>QaXj)-gK7j z`$Xp8T25}N_et_8-IVz6?uWGEDyOO3w{YGS{i#c}Uri(*rDqhbda^#%eoopc=qED( zC)Et3D$q;EiVHQsHDA+1jUK9xz3`F8_024t^z!pp_2=S2d)O0_uzjL*!S(&bQoZ(# zPj~8_I(gqv`B*yLE;wBlatd8<%S84hqU*b_*5~&>_PC9xH7~1DlzlJ(w;dTwZ^O3R z%(yf)?SM^CukX~;@OlH!9$$x87PU{hx9a3H+M`=%Q?zIya{9$4qqth)12kxF_3G;b zoda;4S{IRk>Nm8BR(+@7jebLzqH<%#PcgUMdIQtp+}f$bHT149ksQ(XaXVte)3^FQ z>*N+W2up9kQ%Cf%@upX zZ5EYNUX6t5({URzqf{%0sUVLRnFhAIz&h% zQn${o^NYvv1OJ5SNnvnS&V`)Y0_UAu){UJD7yShuf9co za7UVtKbpduFPvMxtfL>Q2il7s!7urTYAv(5UT^}gUhM({r(VF~6h@6xv%#;9#G02K z>4b?>%$;}LA1WHrBF?XHuDSpIyG*5u6_|!}^r%7X1gy;dpe(6HY(LRpP8~+hI;)6^ zQ<&~xP?r{Yr-%;9lrCkiZ{LWfhwHC3 zoo;APy9}Ss3rT$&&0Amw4jgKJ-GK!xxsclehi(frYkG-!v#;z9g2>)CbnbfSR`y`1nasOo-TYUy zyliTtjBo<>qo-x2^KV8skw0OJg_4N?&r${d*$Br9Jh#e}DaB5|`Dik!?KM{`s~Jq2 za%?5XVs)0fyhSs10KV`7*WO-3~9Gn}9l!PFPhlqqEL1%k}h1U?K7x#+hQaye6STtzJc#6y<3(PLA zQPnJ2w$hUb+u;+isE#87dHU2|-=XTSKle&)!Ixm14ZZ*F|IhJ-$|2|bsNGslH1*$! z;oO^)97j;0f?(3Wv^X2g*sWIK1ajs|B#%i42O`Syhtuw53h1jgle1y~&=$PEumItk zh)2qyyMVtuTQ)K*WjIOOFV@1J3Y_gpfU)a80w`!wIBX`u%VO1mI$N>V@f zE0#Zq!sIEihiL!MwW;>AZ+wwd4rdd8LGsBVn%&+gy% zm=|7oo1=Z(t1YB|>a@A0COH6u21jF*dwzv7&QS?Fvi`AWkFzIgU(&do_MkqQg_B+~ zB&c6d|Li=fMssAh_eh%skj)9urStWCzrq&G6KSO1!@RE=q5eLO(dvkf`v?8L4|?DF zc?a{cIzXTfyLSC%)~(-=Y<2LCl9hlyW%9(4?2wIeG2R|R?*`cD@T;%Bp{Qs-m?~8& znz5q>XS!J!m#1dDpXml=L;>iG0Git8jP!m?U~o>=?hpN-|iRz1?dv@Xwbmp;)DdG23IDS9VJGS&AyeQjvcMgwRJ$~f7hg#4=O?><75!ti3o98oS3do0-$08e&zZS+xEZ|m!1#47ZF&QcZ2ILF zpu~77nzPXBbJ_=UlI4%D4`Vhv@~|&l4!73GWv%vv`S;S|nc*WRnY(-4MBI6311O9o zp^u)E`U0nVkdiW|pDT(VDPPM8=r-TVGauxw++lRxN$cdHXtsY+B$lMV=RnQ6^_!eL ztL4_tL*KJbars)llta6o^W2NrxpU{PfR7o}epHPObM;esc4p?nlnzAep>jeC=UWSv z#I`a=Sf2A8Q(%YMV@Y3sv(`M&=Q&fpTxr@Yry*~Mb-3*t@D72^4j#I{yXo`Ti@X_+ ztB;jWj0;1`=~4cJ3&$C-XC{>L^9E{)vRWKJodfCzpg*`yeSN}jFF;Ma^5WBN zuH#qp5eaGWlF!-66+E3gUSpgrrVD*CH1Zs}Ci^*=!>% zPAp%(G>c*$8@;^t{~C!hiZ^fCkT&`%K&9p}GiT4I`S;B<5hA7Im*-5Zetv3DIezNi zy{M>v|NEEqNV$OypI`0JG~hxlq#i8q6DVK)2Xudv^laJ(UIZ=8HZW$eaX5m_Z>d(dXRB-ABhSB!1T#hcd zzMnW9$8k zg9-RsCL4^gojh-(ofS`e>bJv&jaw*K4jj**UB1t*-?-TGN?B2!nxUI9nMpMU<)7|m(4MYJBZ2jLJMm5)x z?Q=^RY#OBtH#z52JlAplOtz{PrTgg~dS^s5Hb6mg_*=hWlPg!11m$Rsrm9JBV{#3}%4@!+QT>{qeW_QmbRNcY?2S4B z$K%mrv(ot&W%}1e^y*Iuu67?xDWd&$xw)6AO5ZvW4Hyo=84&>(4)WL>P66{*Et+4V zXp$)+2R|N((&(>i+)3Y*a{~A_q5n+|e*JZaa{x|iZEJFRlA?e2{g0+q+Z)YmFF$1v z4bunAIhCaEJ@~t^b30lKJI1{B#z3DXn~(iFFq=)uqT+`)A_w4kQkbT|a)J}EHUaiT z+%QxAu;iur+Y}wRy;A9WgSZX_8FX|Z`C&GCA(Z8if5{nlAMN#ssJzF9Vs8acSSJW%7 z(#7aOmCE9;i1*9>1BNjLW9&7wAtoXN7b#rO<36+xgO$dIc0IGLcIy0_QUl=hi@(&$ zrBr`L@nJC#J7opu1yw7Xf&~j0M7}%gtio==F&0H89g6qGwlv2>6MSx^`eU|jAofuSGP28y*-E>f?|kSkb$hw`*fAX;hz&_mC5hMk@a1+coA~ zdcKvW1!?*7pJ`4xqo+H2M<|Yr`e>ZI!aBGs{4?lPhZv+A+jI-1M z7z?R1iZGm!KbSEquAbBL25-y@7cF6))n5I5$LOenV#SI^vlzz*7L4C?QBu!4dh-}B zE^{_zVu2T&ekHY^IUKI}nkGkAzRXEw)k|QnG#NTEOmL+?#X?)?(e{Wb539IROc#sP zE~pOcH|9%PQ26CnxL-=M^<8hgmi6nDx$?sfY#ejM5pdjxLwr&5#TRrxvAtG=65xn1 zs<4K}p0-Nn!AD-y?6|4&`A+Wz3+6N5tX>zl99H6$??+NOiX4ws`}yZznH4`+aQM<|?>IcE1Ci%nubwHzl`oB{SWo39 zkk4s!uOZ#FiM%-W!zo9dCOom2L)2Wa2S-$Wni0Wc`IS*prrRb(4sc%v>LhZQ@bmTR zmEj$-^MT6ZdL?bHx_Ww`rDTa>28*@{g8r`m5J$pbxg+^MVsmAC#~9;rQ|0cR!lKh4NG1MUF@poNsE^4CWJN&Zb#tZcph~ zspmZ?aI{f&Lv@MvFed8M@j7Sz(xsoe zd+tIrXYL|4xonvdrcwQx&IuI@!YZ6smLkBej_8lJzP69*kJA5t;keG=e7SD@U}`tL z8u+Xxi8rI`yd*`tUPnHpPn5)q@P6mTWKx@5pX3c3Y=+ArcFmF zG9@eU^Z@tuFv+9SVM2SXHkP-1yU3YNTFaQ9=o)|dwSiuHX*KR+U zp{NGfu#SiX*wD%G;lSBNM}#M8e-^g9tciG$GQX!s$I*cvTY?C&8kV&3$Pi7;Aau_T z&Y&OF2ON&TU(p{VFmA3>ySk}LAK$Ua8DpBOuWV)p51(MIg+y3BGjJ}%`|<7dvfUy2 zBB{4#MI};-E1#xOeVQLl!ABNQBR1vd1e|s^GcIQ=%AY(QL9*ZX>O>DIpKuk_}t606Xm0QAtUt!-l;=tAOv~LXp&pBSjW?rpLvBIl@~oM z%;(L=#Xc%8B(G(Q3~P0Ckx>4hfBwZW;k)e1uMLR3U>%1&pk`3{fOLSKZFs&dWE;y; zq}@jKYgwmkEO^dmpKsra{y@+aw*808(q^U%=|6;m1;{8Db4;~r2Ad>3^2Ceu4&g11 zjANhLi!ral8MLq*C(2G6YBu4>bg09n&y;+O3%IR-diF9kg7du=N1qn5aM5zpoEEnO z@E%^HS{3uj($5GJp}-!&k30HU)3{M>=En~f>4Wu`Uv|1YY(l8lu*&N_pZC;YK11gC zNSOais!9^cs~*QE!t_h>KA_R@-?k^{Kc(b)i?pA#Z=fCeZlL=~zdvY^DDJjfJDU3+ ze8%kicfYeHC|@}x4eCd@n92mb9w_VoDK0mVYX^A!P~!&ekx_KjqxvbOQW8cYwl`W@ z>3X9kl24Yd?O;>(&O*Cn3QkSI&5=ldl%7^x<$_etyX)8Fv}p6>ruuwv?P5ER498V( zZpxjHYjKaOVzZ#opxr+T>?6sOD-(nVil`J6&mJ2aJc4eG1;lG26F z2Z`x;wg%lI#;0k)RZdeaPx0JT+BIG~;5tToq1{2xEL`=*PyhRGf5-tC8{(ACCI?`Y zeK5g)7{l8B2&q1g(LV`JnIU@0alajg4j)fzho*b_aK~cmKD{R{AapR_O26wIe%asVcu5^y{O?zDr~JPE5Yx`Zi|T^x3BCO&yfo zOrA2sl^Z+<^3XhV1%ES(&I)A>%=$! z|K;^B(fN*)>dk0A-zI-OTcUFE<_-GYy|nf-vtYx9&F0d}+L^P@E^0cqZ(}aJppt># zjo-&&p1$)9*YcR?^}Z=vgO*wL270|a5QpCF7x37lrDN28Dk3BB~nMyJ|aic^69_r)N_%VEDY@yk)hEO^?O9kf&% zIu!1rY4@W~QYOV=)?Q>N+e?CUNK0If^j0cZ zXY%D&`o@$5Cto=GCW4&q1gv)VeRF{6b;oUP0b|XY7n!kRC-Tdp@~Gp8;?Xo8FMg0* zjgW_zO@*|`t86r#GiRPDSg3$0TZWt!XwW`rU|&GdFQN|v?!D)BGiK}r^BXOUjshHj5RDpt?XSDWTuqMC zhy<)V_@Etey}mznoN3(Til}%lp4$QkbqnM=|K`xshmnlF=*3NmfhV?o{5UocFVIK}b7 z@mZaaTQqMH*8%t}+Q>tn$#7-|Wvv+{;LV*IZ zV_eh54b8wo!#t`SVqxcFkKE56verGsw|=lC{(iG=`X#KV)LNBJ`r zr`cj>9~=yW=%SR*khT;bYj_^o)6eRX!_im-+FpTuY$j8f4zt1P^dZ-n0fWby|2=V! zIffS09d{fYe69V;Z}DG#-DS?Q^eVI~?8=qo033nS^PcLWYwo3pFZ=eY0L z$(U&$%ngtjKINBGpDwZbl+UU{J9C!iOB$7Tc=Wtjoaw4Ut@QNDxK$QdhR3bKtojQS zID^KmH8^txja$F`YBi4;E^FS9^ROw$lBFxiWKvH5zxiez|G{CfPA$17&m(8DFIIi4 zR&c7O1*skhn_^j;q81oQBsxn`dRlRnOPq?+-2vXpokbhZpa0Ii8$e58-!K zYY*|Qrk$Z{*TPDMJf=YYGq`L*h@$lXMiEXZ+Df^yrTF-RnI9Rv=SsO;G0x`=WN2Z{ zJ{lKjxtg9wPVK@^i`!_?qmFeYwOo|AeAcKEL8?bA!R;W7z8=*9ik2_@K@M6_+R@50 zr*eZN$3c!Umo;z1_ww(*|KXy%g1&Xz4;;aH!NO(c&f9Nfg~c%E=GiBu1omVWu6i}~ z?bkd%<#zBhT#IRDR6`4KerJ*_Fn@2kacB*oR-al<1D4m$_q)J(|sjlkLJ!)cF@#S|4Mh8pX=nOOoYr z-h5o;gFGuAs1-R6$D#6X`))fQ<9+i;o|idM=4Cv7C@4SU0<;NFbQ*KsdFMzL{jR3@3fPSe_7r}i zIV#z#3;mV?MKwC{ByM+?!$lql&3P0pQi%HA!|A=B`cj=(wf)~UtuALrg#)zM=E-MX z<4qc*&y;6%e%EJ8f9cCtu2i1-%+gf!0rSDkdDLf;({V(UiB^l-Hi-x%-CO-|&WGC?mMu>^z{Q+tTQn6nnivC+L+cpGS4G?1m{0W6F!t(~7HH=BIq;x$7miyns&kCG}Tw zp1~)0l%9SZ#z=q0n*^Z5Y{0oCw13VqDYq`SM|}ID^t9qCr>X6MaqM`li*oM9RUn-sn9Qa>|nIK$TX^PuT=CN6JPJkUd1DpI!4HDUQnrAoW&@&t02>kG)vbGiH zqy`1a>(c33nllRKy*kon5`Fs(rg=TjD>JhVOQ_$w1HHqz?8(vN3gxWK&9Td{oPnU# za#-daw4mw16AOy|og0mr-YsQit(z7&07Ie>O$Q@NFreD)^y3X@`jUIo&h<=6_0PuH z#{5IK3Pre;1^$v>}#2o51&%*lMaeLyP60|X+2AjrPOrRE=pB6W z$x@EMcSEOJ-GBzsZ*RP@qsNsshlpP2M=-pYB8I|^aGw3(n5?E;hTIm&`7My^ z{F{T%k&a{%rk3F78I0$ErhyaiVv4>F=-HzyMN_*Vp;ZTq^Y8wB`&=E$Py}8dHg4SP zoLAw1F?4W$pUlTCFK2CS@@42Cao#{C9C&BV{>afsjvD8V z(I-`|biTv&02_)ZY}~X(56LNUyK_3mSnSCs9(FW6fTLl<26YsTNquMJ-uK?YQIVre z{oj1qv}t{%x%#ThO})A`qvTA*b(c-l50eWA53bEZMu#A6xJD!k>Q73gNhF2Zc(~?k z3R}Pj+ylmuk36MQiuZuG29EML?I9RFzJ;GiUj=IAisj7N#S4e%(0@mdPFC0$j)?2Y zCo13N+wwk|x7bbC=Rfmwa}GsXQ9Hm3wu@_2F~y4&CTG6W#K8yo^alq;h4$CBG)EnM zq_hJOK}Ynd=FRFUi&;811${`Hq0N%jwXVrP6kX{GUnY7I`LBYM4RR2$_-wCd)~>bq*M=O|Kvoqic`QmA$D z1u<>j{@N=z4@%(tE4u0a{AGvR-5Ghe-O}D`2f21Gta?7(=S5ukUy+lp$|G8!0;2nA zK2qGn3UDsIta*bF9oJ^34z0++@+fA5W5u35e-e+B%O7s70_eNBOI!25$M5EI`Pz$p z%q!17WS)Jhx4HMun@z*|7dtt~15rIl5#{`E+Kd2@?~J@m!!exi<1#YvF= zpMAn@(4ssA3l=bU(gu18OO^)D$KfN#XtuewZ7bTDU7XHcCLKKa_`^J&#F+BiZ`U}| ziWRHe@ukf!amW5aE855{JK)1vbJ;N(Wgmb1@#du$o{ljCPRp;n^gkX;Vm$euzb$MG zcj_~A&I*#cX^yW24k!xoKHg%y+?|}VlY->RNJ=(6s>9GTgwE)-zC_Q?l9Z3;CjG)m1w-Ft?_Qr8CxE1BZliyEc zQw2nG6g`wfJXnMWCt!uU$k8tkeSq_MfcNNj14o;vMw{|zWWhiG`pZGt5W!AjMA40(m&fF*23D_wT*5kWj8$>}p@N3VrDH=olB4JEs&!CiPE-%C8~^ z^?g<}zLmSKF``v@r;}`j$%C zU)c_lReQ0?mg+z>6)w%AXBv)DQI4i;l80-)rkPBaDkm;qOcgrXOE!lS9MPGg@}T(j zONw5~K5y3HdBbujAtE2bqx4uDdL$QmV(HNT-S)NO!uj<(Z(at^x5G%-mj~XHpKleZFQ~}R zH_#CYq}m1N)4frdo^O|%V`(!jUk>`Wd(St_PYWHC0I)FnCVGcOuF{cSK#{diJ>?|F z_#HXiBmd7ma*u%^8`_2KMK1w|P#;t;oN@6k4Z!{0>Z)eMt@4uQrA-+wzNiY<6Yt<~ z0*3smFIT%o4QyU*`B4s5nL5-el>_#d?(J%i+59@qh={ zB>l5=e=MaNL`CW0>+L4G-VSr}FaUVZZk^n72b+?q{*!5uefw*#(&EjBb3UepP+QC^ zufOBIYn)9BW*gS8$#y}XU%tQG`DohMGlm}J&mdgOkrc&>UEzGX$T**>7ow*XSGn9Y z+^#8U4>|?J7RaR)S2;~_-l13V|34|Jom9O8x-?GEPTE*1pHaA$qx!N3PqI~tJ(^6xsuaV2g zjv44IXA~Z`C$0JeE#jc++n2{+b7h5bz736xSL zh5XsHp34WHBb+aoPZtZ2<3qrKbyNzMw`k0K0afJid+|l#HF&;W;n$iUT&dPQBU3m5 z!=X2&kX_Eduuslo+S8QQF{klWt#ZB@(Emkq3q=&iaeATU<;`5*1YOTQ{{}~-MO4~E ze^<8Z6e{+{&ew%-f8%U8=@+3*>kfWBk|ac6?XJ99VB1&!8RsVI?nUM6^U~ zilEWOK~tx-C|`PTJZEavtj>RMp!NaZP!=p)Vmfy2Y3_UQDMx}|NLjnFsmsi^ga2Q5 zV7%zR+Vn}$lqVz7d=-o--cbH3)84Hm8mCO_*(Bq`FJA>%ey;f|i&9A8d7z)n+$!l$r#^Hx0!IRe7(!}4@#6M&O6c~4-4kL z`sy1;-s#@Ofz3;ih?JnaoGx^|L3OB9*CtUyK zQuC3Xbyg7{P4_Xvp4TVm-`{`#ops%GOAn_L&SUlJ)pj@zy57>6uTNZ~=S7@*w`|$Q z4%N#){oKhQieWC~w!op#0++RDX5Q@k5?2M$ET4GtxkI5^hfW=IPB6RD9MY2N1Po^b zKSFNQyqC$@LU@)N$mCQ&^eY88=yvSTPEk(p=yji4#8SRoSu#x1E5c7Lw=XA@Ktsp_l*WJ2yG4tk5Gx_qJ!8A9(lO8Iq%$Z=Bg_%GsDOcyKbGp>GvVpTqG+& zcj-L%QaGG$rl!(POj|8&I7`t_&<>(H$WH^VCW$DU8c*MvwVTKRmS$X{!1jpnLi(`z z{SQAmjvlvmG@Y(*Wy+N+Wg0iAW%@kOgLYXxf$4}eI&jFigyo^h;@N@BKllD^W_wY8 zZ@u|Cb480r=1htfiodI`Y-+B#y1BDs%KNijIm1T2OS@9;V;Z8FJ@(M;=ISe&nsX?M zDdg_I=T>*G?%cK8y#K*R&W>Mbm)LJtuXlPoTz8eZ;_}8^CZct;xU>N~Nje5Pz~-uc zJq8RK>!Q41k>f-6-Da@x6_HLGHK=JG?A^^^M@#_hoTJXpR!^XhuL4hh(m39~U`HbP zC_TVc?%+td;p~spH`Z>DwSGQ-Qpd`V6d*Y1P6zInmq;J3*MGe0^-;cFn^4r#+EH4Zc|`u#Uw2wbW6mJQ+By_%P4(`fch$M`K5_I$ z^=p|P6t(8Us?iCan^vEu-3X+D}g)qMNaGP7#MB63cB#zp6a1jc5!M#fyozw4d{4IJD6a5P;wec}`QaQDHB;Z8;f^sb#2B+)T|7hbaa7$8-k%V6BN``^$p?4c^AKIv(rdK5{8Fw9 z9DOmK#JKpq$z$9z_Os7d^3L=bY0|D;zd3bq%*HqxoA4-1diQ-jG*@2Hg3nnI+O+TR z!w)m9TU}v3S@2=9bB4*4;R=Xzklyir9F;jM3+EoEpHmzsg)5(?QGU(O#FSJkl(doc zrqB2&VmEPw>XAGy&RvEbOoH}EUM|UUNZbK6RakEda3`$1NDr9Z6n#$to6ua>_6l<( zMI;pc#taxVnxjX6N5-u^%q0zK%ea-E-4FKe?#8W%P|nYGoJv}JfQ>W&crHKD=T1|V zc8G`b5XQXC8rL&dwQRz4{CvD%sgw!C)i2I*Hqvn!FeI=qN@v{TjZ63HlTtby{v`UZ zm;>i^qyRr#aXZ@4~Y(0ee( zAHs7}=eFk3ruEDTCmhfBd~J%}{Xp;A9R0J;zocV`5kl2$lYsr-k`mr^3cTXXw#(VB}*0~u3(ch<|RBNAHrdQPmjfk3=a9FBtE|} z<0nn0%^Pseo#;4 zkkrp-8BdAgn!X)*zCCz<51NmuZO)`9w+-p}_P~AJIRZ8`<9!^@H?5cde0!YUGpka) zj-uxq=AxR?=3(eZ6y`6K=bLAWlDPl4^NyQnc>1ICwBjn4Gg7j`);oS0rDreBrF-_K6<0Y;alQ%oCw`h7oi!yP zpG%M5u36VEbaIkBKT&#Gah1zW-AzSJ!Hu763I@O3(u%8`rdqz@nUTVDUvFmNsy7o- zy+ODhfqosYyBjE-bf?eiakwqt;i;yR&1^Q#0}gDW_6ka;NeRi&qx=rHauM93av2a} zOUZFKoLK!Rd)Rzu8jU%#8c?Qg{FBvXr=>i!8Jwr(^08wEQj}46*e2CaDPKq_!}od4 z-RD=p_sX!mY_29H6~uf>gnkiijEHZZKEx~k<=5VJEQJd}k3he-huZ62?8PtSvB{GH z*7Wr1d6Rkb_5YdoCJiwQKbmY-F8|Pcy5s{h>w_`onWrA0xt!xzkIDyklWJkhq1|gA z#>{WKDd+-i0;cf%{~jiXw-?s;1mWuc9vzVYY>IB z2SueJ^+7S8jy%D`oXRs|)FelM<8%Xh2UD8LRi|?W2SXoxy~>x_snjH>Pqe($(!rlu zkWwe$>~#J`-L(bMnr2q}Wu{(r{zZDrP1my{aP%HaZTW56T;W8}_f2|tn!^(vAi}TT ze_T|(YKV2nu(2Whf!;ksxR2b?<0kqRelh=H{w^utO>C25NP;#rV$%!HK1y>+dE!b8 z=>I|}fArXQLwH6YEiS#py!P^w{5Ndy>oliyN?f(mKcKa69$>ydXMpsi;XBdK^N(S% zkK_2@f}(X2+nW?Eu~h0Q%U%o1G^Zg39A=Y(lL}G9U(XHbKUzw;J6O_fjy^e%-t$v% z96hRh69qdtGMXEUfBZy{m$$SrXEzG)Wv>Uv{?RAT`Hj|CHYK;7y zTEeI19_O?cShIG6-LTluTAj>_8 zoK~F-w?}(LTDg%6nbiV*B)-Lqm$4INqKLl8SK3Q2zmZucWw_pR%9b`Gh7WW%4<>Ow zn)k6QXXeb=rVTll0NS*^$_yPgI^^JsMMPsq4{`F_w}+nw>6Lee(7}==%Y4*4`PB3D z!S8b3U7n-g_IuT|Y1=WxlbMLWyDt_=U@@{k5rDGqCC6kq3}qz1`Pp}@UcUTuc6@5s zpe`5l`m4{G^UlA>O-5m2O0Ui5pL@bIZW0XM)vn*&0vuWhyJnqc=cc(Wa8S3vl=r5Y z3Khz89dez2bKt4ecj-7OvJS+_=UPyHl%5tmC$&gQ9ePvBmz{O{V8(2I7UG$R1;KqD zy4T!&XLnP+g1lAEo4?T9eeXjYQ4MyMrbRpV-hF$F$%u&7z2`kK`N()@@Gx^j$99gk zcI|ri%+t*2(?2vf-Plo4S_Cx99jE4C@Q@MA{_unMaD7m(iszrle^EX7pz+LeFPmlF zE=J1J{PH$t-kkT@X%`M5hy;v3A20^%*!fnoW$QLS+l&}F*0jCmY6FL9031;8=L4b+ zV^fcF&MD((^D&$;8#lX>onQ(S$WI>wPB0y=zn1@^%)>^EiR*M7<;zGMJ8K@uBT*!9 z-^=|RA7>&Jo`^q6H>4LDl|Q3#iAGTu7I8W0NvF@4N7opsiDR$Yt!cDAny;zSSsgb@ z5T%1#nA^pg3_9U(Ml4shBrVv+ED`EM+14J>OFKTk*x&TIzb8#Vo@9<7hdV?9u3Qn% zS>cBaj^2EG1T6^NY2_(H3`uc1dFqF>`1n|gie8CD8`P^|@E3*Ia~GK%zC3mzd-jF5Oz*pH=JF9Ktp!DJ#h(x8E8vW&c5C@TY7Mf9W6W?|%Q;9= zuMsb8T-U>b!)Y@If#h%Ow9FK zjjq>fR#FoGdW8}-SM`AlQT(u|wtTtL{1;L{$c$MZn{U{`J&*@Jg0A`N+O}l&f^=P9 z*1TaN$^nv_w`A3vjOnnxKqd0rvHFZ zKuHgJq2L;rHe)V5GbuVUQziQGj-9{R@=&hjkEXU?ck~>B+iGNMQjInpyFFM(o}?|{ z%?|J#L!;%R4}PQX+vddt^LD@d@~dfnS!>>;1`z=9(JkJ%2@brMwrI`Qsb9wBS6su+ zyNK|MNKx1@27f+a-stu_diz=C>u9SAFrOg_%9)BE-_XXJY@q$G(_SnJmgXqWO zowGS}=d2FJ^}Y6>LaZsPwDjG z^3DZ9v%h7uTX@$G>@Q@)*n zbapxY`O4D8Z;ZLi3c||Uq^fYm5orxK4b{e7(aEvYOpKgDkypr4^WMyxl|vGokS1Tk zG4tW7$X*8u;@7;g-_58B*_``dE9ltNPBN;}Stc{mcS_YpLDA`)XIo24^JhoNMbN+w z{FfW^L+06{$OZat#g<8k-NyF)!Q7;F%$KNh3@RySAzqZw@qhOr<)_ zJhE6FzSrO>up{?Ay3j6S)@wT70)RX1_2h#^xlL(FThL5^_W|8EOS#JhUQy&1j|pOf zFJH!)DSNd=pc?|6znfvhMAv68_zk~s(r}-7Imo3oHY6C)1&}GIw{(<+Zw7D*=CEjek*euuvg8K+lm$X|J9--P)0sP-zI* z^I@1Q71^JG!EZ12KFw3r(s~xozb6ws9;SZgUAzV_O?kYgtY8E&9b`topJ&g=y>wgu zouVQ9vG(k?S;lHg$Z zik}wLy?-UB6(x2hyM=Oo*B<<#(L&$cdi=@NK^vvjGCRn1=ntn8UQVvzZ{?8IxCtK`{S{GowMr;u4rQYB3vy1AG1r66_sUz(RQ1vV74H09Do zHD3xXFAEN>_5hNR1z@`O{sCEWXe)Wcl&1#(0Q^gg`)?3dyPFJe&fSyM7kTtJl#9r# zXG{#V%!Ql?-Z=7Kb1ox=c2}=cv_Yk@k|^-XN^7n3R?lr^xlG&-{cozgP87x}YKT|7 z<6W7NBU&kkej@p9T@|)zto>S9jT5cGdNj`QPNK$wc@5CZod+G#k)8hDVFwdm=DWb} zQu5CmrwJa->HngffAlQ-G!d#R^Lg9&*%`4VfhTg=GFkM-jwTcHoexJw6Myb>0>XX! zm$HII8zEI{iRU4CB9h$4`qCP&JvUY4_hWA2b`;XY7ot6;)H>5`>-`q|;j)v(+T`^I z=C~&a`im&-tWjabQ?pnWJq|yi@4cuu5mG7%TZ*^e%-ecUybl)dELUX6PA{ZXRh#(S z!f;YfJbYagobAF4ecUIHYNqKWzrrC#0CR`I!nv8|YXaz@x zUKIR2NQ>eMJ7s&Wjcz*QF>lP^oV2L^8(t2X^tljkns`(iW8r?lAIVgmyZU$NPTDJ- zwa;S9p_Do*pAF_RqG!e0LOJQ6Qf4S$f$dmav3`9P;)w)Qszz>|U;Bkpwa}aU>DK=3@y8|CNRgXpA@GRKZPpWxVpXWS&@yw| za#nDU%%B`1zdCSsuE^-y;mDe^0xg!bc$N}x)~4Oiq1y^(>92^jYZ^rPA=TYYrc8_ya0_q@a{hH^z7OJrLODw zi|v2g00c2h+)6bcfdi1J)k*p74poDDUy>=mT-vFs$vJU5L4M#_Bgg(QLKqzX|U1BSt;i^;3 z3d~(KLG-~dGv}+RGVinD41tQLgbUu{#%9fDdKi?o%bW0%7YEX=sH zZ31Te&z6#+6kaMVrCc?HZ(#GSQsKz(-5?-2plemQ7rb%|5*AZ?@n4U6!PS{ zm!9eIblgK+k0kE$Cz`^4r>z` zqD@9!xo1lDD9pZC@EKgnohf`}=FS>!waXms#RF|UjN<-hsza*x#@_?I?mr^9C! z$_?)eo-gIi?>SN7F*hGDzaH&{*mPK+g~+PDrKBBhF754~)@GcP2^Eh#FQla^B3*qJ z{Bf+GcWi9rIs3`!V@bFYrv1zAVL~qJy8+2lG}L%D^K;@jn-tmN`rL#Pil z9w&ZCMl|DEsr=0f&pEHkhWugYtBYjtt*;qfF6^V-1rep`IlLkVC)sWIS4C1JbnZ>w zv@{!hCn-;5H}B}(PiK8Rh&V1PaBCc0C~Zu*WL^1#6v%*cVaPNRQBcCvpa1U&pTx?u z9f^dGyc(9(-|@dTyS(hFze~-sPe>YOwt9+6@QY0%w$^$6N|oNG|norKw`{W`u67ST(DFp>loRR)?7GPEzkwG7 zB^gt^cH;F`rXO`b%b}qEM$Tw5%B>CR*S4b?sxFCz@~E)hU0frhZCx3o%iw| zt#aC{IP$AdJxiq^YH)Ottl%+K`(~_!7U#^jQn4R_l!^F4?xg&)2`3zyy_j_xujGZb z%4Lp}E#>6mMKbpv=5^)YPy2!3VqVG_1K#j|5c9bA3YHx5P5_$+X}slq6hprlatJ(| zp##O2^54LddsAGj+Bh^k}7qlK`WDf+7K=^Sjqw zUmQvJ!FYuer*6d1KAVjna;)bG|6 zW*1g6iM}f9Ruk#hDbunIJn4LEMak3?S|E6NOkf$dDVyum(`BTK&~j112h$je1LgdUt2b=$&a0#xZA^%OKaOd2|3U?VFfMKuq^8T?6~Q$ zDBPo_?*20@&vmrKemef)!)ucY(mMK_9Ty2J+O62ffhpGX3cc$Ab)((O!Gg|p>5V3xmHuB4H?gE%ZRdsU9PP>|V>An4jU_k3{~sP#g-mY^d=!DfSEUlncHf4X> zi_aEaIyNAfj06DU9?imgv=NjO)P^O!R{aDb;sx^SZ66|h`x3|l0Hj(Heajdy5Z@;% zkyq3Na)0bYBne1YgAjJN+?NLPm#Z7;*Uc3le4h972N@ z)^y^*Rm6fQzJjZ`cXLNTX3jtsFeiTXsRwWv-3{tUJr0sn7$&~~dF-q^vfohK06D>g zUU(`{ou%i`zz(s`dr>|hVH1sO5Kb2qMQ;1cWlv&Oxi5Ye21B_w$8;bGhUAlmbn@=zyR#r+_rAjtXTjAyIuSunI|lI}_y>sPn~Jjx6*&(sLB6!oK?s9Q zRp-cA1r)u(0+DoDs^E*=YnJFGjh%7`=aPNzouCPt+|~$c_beAH7y$~grz>N2nQm}3 z{kXZEvY_dVX$*U9DTJpFM2|-FRWF>jvR);tfnhpK1 z`MgbP;@*c+*`#6x_KXPtGQkaHFB_$K*+0t13{=aB6&UC0-kDtU(%LO}O@IuYo%Q_0 z>XqCT%I9-XS0or-XcP9imoxba!}aYZRK;Ndpr(Kp3*c@H2lsWM`=Kg~yxF#+cUSUg z==fNi!UY(8)-7sS->o&Zd(Fty2A0UqJGOjzAEZdkk<$m^?wihFWOH*S z$(^7!jAo)aXJQOj3O5>4|57 zS%P<~i00LI-62Pp z`m*_VLU$8}=E*p)Ub{0i8N5uN$Wo{8dMs(kq@!rOF(`&*R}c>y-@-)9zqIdr%n@A{ zF3WG1C@nNb&Yews*4`&kDAt)+6v!kjC=>!m;$8c7o~z@OIH3OmEo* zK`t_n{+Tzuu5L3p&3k(?OLtXmpv0y+1#{l}R-D}<+9yR4mb)eCKULm2ST&;B;V8yR zV1q%Nw5d;6Y&z|s2&i}v@bY-#w@T}Gk`P3PH|s}tqPVMz1z)xqqZSMQ-1@|C-hK2_ z*1k-UC0xUWOGVO)yX8oDG@WRaTE2Q^Y*j=Dl<7tHb1Nr}-`n3$xw| ziP1Mz5Cx$7`x=he40ZEfTZSTM^w+f%jB!E|F!uOU_a?Lhdj%lWU| znC@v7X{RO%crRo6gd?j#cc~RXnrk@OhXkFMt;LadHHT6WsL2oe`PR2Hq(XWjW{Z~{ zD5TBh0?V0zVUq8d!zyAs?u{7iuQSKL8q^TfZ95Z^qUk#Bl@~mmh#C^#sKvrkf~mR$ zvvAdP?Ew+ZsRexFtUqdVh7?s6t57Z*W3(qVzSk*3>9NqKy{<|gA;;HmTb~rB-PA?6 zdephrQ9@A;CoAXILiYRIldKK;>5o?tOo?anBNfvDVWw%NWJ(;x^LT zooq8cuzqMA`~W}#y89W|x9&LI*tjD$vo7KYOjfw7H^aU8K%*Un*)-4I+9#ad1`?e- zzgi}i%jr2P8SqDib7aM0wJa+`ku~FwZid`&WGX|M8YeqbS+~D-0;rGEKlz8ED!qj% zwhvFg4c5~zMFoBKfS*pa5nq&wq8GT%6j5p2Z>I6`?C=DgKE&^EXhZVWB7QsbltV#0 zyDTS4uNvp6upverG;&1uX5fyZ;2qeZ+lyW${fWu}S3p}s0jKo8!`3o5O*XGSI5ffj ztj)~KA=zcN)g6s%vWR8xd8toO`b2f1)ZY7?I=%KO$Bgc>OQM*AVxj;K>2~^>IZwbU z;5<9#sgOAln!0OG${!iA(2oS_dFRxx5$mV_?0gKWj=@adn#b14(GN@r%H3e@xDqkT zk+JYV~Jh;tv1A@g<2F6wnN@FDE6oM8z=~_T! zS{7D-wnoqk`}s8*lR@B4LC%9k14P zIFKZUPD3vs4%t_f+SSIMC(f+?8oCv4yaF{p`ZBQHVur&JhWbbi-cC_o4%m7x2p}Zr zY#pc@owiVwnqFL_8(S|}endQ^e+JN8jz3(Ytc^0B5%ge}^AthZe>;wL*_2zTkH1%- zZ}&o#fYW6waq@D9Hx2@`x&WE8R)`nF?#o@vgRs2Kpyj)6!T@(C%#vs?mVOUoZs!-; zR?v84%y0d&(^#Pv;@u~kON(OPrUSMwmE(R>kUj?X4iWsl>s}$c7i4fmv(D<8@SEd! zt%#Sp9u+dUl;sIZ5Q8ST3GUL2D_gw+|DMm@{PZm0v)cNB>SXJ49H99(oH-t>A-mN* zFCGQBMRh@XR1d~B`fp!QfF)hc$VO5_^p;>L$Zp-0@COfrRS|+iuLi{6gLgR${Q00X zFWq>7#(C;mD!zZ{RUde$k)Lx}d#wvLRrdE@E<9s7lKN~JZziodq>pAUXCH(`RNP{J zOR^aYzLty-nYMk93H1q1S^i{ohiJ%{JVmg@_+?>mEydS5Q^(gt!L>g3?}GL`0XfID zmOXjdR0Qt`dcV6*se$J*Z;hGNk=~%pa*p92itXJ-$Nuc%GQg)RjLP;vnf(2;zSAUUGq_Gm4H`*H2w$-b-rT4bLQh%WaRXY3>;h>)nA67dwUvBkD(}{R6<#%jWPzu zqBz#uG^`7FdYvrW3SeOMh4bR^$X(WWFm9bD1CZVXv4-ZUXs(R^iIOCGJEHKRN~OI) z@gjOh67joAM%sBVxf)H!ddy|-BrVki|8hmrbGYoD3e?;TiIGYVuuCaox0KapsUJM< zcM5|d7lPncnfbjhf__7Q$>&FD%6wE1ig}wzDTBpOTN67`luX>pBNf`SkS#2V=)n9Y zH6&QM1&DsGfGc9&a2$&INyD2Tk8kv5w6a3knza>N+8<&JBp?$Y|M*$MvTkY}D1b6f ze6!8@Phv1#M#i73saq?gO z*)U$25aJT+QS-%&uCb64R&D~2&sL%3M@U*`95%5Ydw^1S;uZhv3^AR)q8W7j%Rck2 zj-#R6)l4ekR&OzPG;1I0k1A|H*6;7>82}DK`-&>r^^C)?ceIIf3R$xw+rjsd?6jhG zq^Z}eC2DIQHK7{ayvYULYX)S~DAHI&%=ee2^Q0&-4h0G{+6?|~b?gKRzVZ-rqLoVk zaMOt~-E5d#1)Qx3m2st!-$4UngSVxj(w*=KmjyDu5wV*}wB(^~jameERcgL%>T}5Z z<#}Pu5n#57a(XrwMNBS(%bRgze|m#ha*v)9+m2iBgoA{dU15}KW0J-vZqXLQo&1zs zC#ywv2U@&KbJ8lFRc2NFMLa=s&0}m|%x_MP_zN(xUrsIND(F&e@4vmql)SNgq#?Pk zzY_ok{Hg`H+ZosY;T9^nM(ODRO2;?Uwf)o7b<#sfej*#kxTNAOTdsX|8u0LSWGefK z#&OWL`jQ)?E+K6RwAH(F7phazQ~DqidHNkxt$x;Zt1JU1@dSZbO{{xaKV0IYuo+Yt z+{H50X8KC2_}h*S#jfoD-2ax{By*&M-ck)G^v?5GuJSqLgdMZ-l09zI@ZsB-yx-Zl z$)&4zZ24*mj&Z+sa&AN|Q4zC7MC=S#N_I}BX^vGkdE@c!we^EdFA^OCo?gJbuXxre z%6LUKCdyWxS-1C}p009R%A%T6lZrS;O5j@#or!@zrOYxzp+d9~e{|W}0`!+ydNyol z!h!V>e>hWfdEwE-GB<>zPjrS9<;lA)!58pE>B=ix3QmM( z`IX!Xx7I9O|2j?d$_8;8N-MhX@R6Nmlf-3#RpnFEp@}-26|oxLedz^C$S!!pw?GZU zW!uVdGm_$wn=DU}kjHrzwe-bto=9d@#V>W#l&J7PqC|ZPX#@l>#PqV+cGaVA?D_(`8|i7S}c(r z?h|5C9p;f8I;()S?sM$qS8wK|uXz)S?+v)oQwpSXDX)T>Dif_wpcjKR?{MyWaL>x< z#OSYBr?#pN+suA33Am0Wm=n$-BscD1%s3Yse|3uHZN-!8@>63D~pEu=w+#P1$KGnYBCA)=@%qOJASS=MK;^4;FEoUHv;=`E6rU|LTQhE6BIXy%j)Z zZzpzE?ihFX30~u2_aQNzsp(rx(7K}PHVSU{D9mq7e-c((;ip2b^DUmf4d){0Tb_Zc ztla|m8N^+V4oppop4fP4VB;k9SQ%jK+5Jgl^@CMklM&-%`L8FJU%caqesZim+1gU( zjFKy4sT1S~9GBYe2bhvC)1id}rYw#sx|JpR%&O~=tH(N))r*E8G{DAsHuKg^NZSkb&PMWMa4l%y}cU?WQ5Q}G`C=Dc#{21@ zT{nG0!cpr3MwnVXJ?94ZqR`1#Rwn03E{YrbWCx)~me`4c=^?@A9sa10KOh~mo4EOZ?^@)jfLcUM`Hn?# zBkOD@_#*}Dfr2fjtoFF4k(0Ii626Y|$68tr;$Mz+zF<7cG?iRpq zPSEZNbA-w&U^WHgmO<(fKhBfGWOZei)J&zR$mx3j!nr=FZ0y||nnNcVfWVb^(9_uU zOYcVb&Y?JL_?9KgAI18O8v$D&B4#M|C#)Y?tYBc|7evn@?iusJi3RxlP!V11JTyQY zaUY?yh?vMjpetptL5pb;V2$ke%404{fF!2bqd=)i0j}o<6swmKA=?GGr^77S^mH5+ zOs<7&VXmnw1!M!Q+sJ|KPdKSN;$bYgY&|+Mw^l1IF zlOTW*%btEO_eAd@J-I9}5oP8--zs-2D0`tyS+&s0)c+1>F3_N9eJ$tz~(Ewl$Fj(wv zLIe^OTp%36pMEuZ>NVP;fRt6ryd{d$5Ikw?usK4$g@M3&jye;&Z1PZLA(RjVa^aFA zW;E*-pm#tHtbL@65lp#;^H$CO^Cs|(OSP*m?Ea<=oFbs!&26mR@|t22d$BK{BmN#+ zvH#^Jow|AQ);b)7*RhAUTM^Hxa*gPu(uB)bvZwjM*3lhT7EK%37Rwu&azg&i zw&day4H<5&m<;q7IS=bkbmac6Wg}Hcx6LcrH3&p=rBa6N5_(H$DSJC|;n`i$O{G)YaW?CXOTo_f$a1l^8uZjes1au~XET`9 z0(P>RM-_O&UUVq27h*8fnT4sP?3F3&E%(U-^aT zGnuo(?01wP2N*$pRqx_yp@+mzQ5>nBVks<2rmP)XRy z61#4wlyQSY06S%~9#N2y{f>1xY%Pk`@3F&|R1Hk^k}LoRTt`oLR+aZl98camE*Tn} z{3O$Q57BDC?U$SK9{)H^_>-UrlT4eS;H62Zl#RyA-f49;dPjzvFLfl25Yk_lW_?0$ zr7fy1-07OpjMe~?%X$VB-R|VBLuwo|>1Vlzov-QdbUdj1@nhK`cog+Es?oWjy+&85 ztGIMqBUaS_xT^rb1DPvG21}5=9VQ0<+lbr7c$`!$tW`>Jn^wk5?r zR$>q&Q}WG3c@JXwXe9YO^`asaSGY!AwRvv6ha>oij|M{Hu~#z&6xPIeGR2ctJbECp z=dRnS9lo(oL{!sf%bi|vu`le?Q$M_)zqMYT6Y+xIMok}cbxIJQ@K)XvMV3ESw6f)l z=-6hr?p)UYiH`(;6!)G-vwgR$0**`Qn+HsX=a z`B!gH)$-Br$7LN&#SVduDP^z5$eqb~b_TxzddTCb?N|r`757F*!WM3+C3psKb!qH!g-^>;BH6LAno29!+ z1t=}4x-{e) zy8sGD*QZ)bcLb!g7Xj%9iCLRky`2PPo&K&PJlCbcjsBedDeG+C&X||}FxhChs_AZy zI*B0oKEO(tlFfIfwW~k1tS+^M zwm)>@^1#bCovs3pRVG#t7mvQW+Ac#=fL!EAAe8gl;}v@3%!K>o$j=5!Kql67LJY=# zu$o3)T*^#Mna_#Rrp}`L+&#qpwDGE+6)FaN zxZiq@T-dV63+*1sP)M$1Xkb9TWax@2_h-pnfq*UdtQd^qK5LjP*%}_)QciCIS8oO@5`wYTmS~{bP;R?{zUSr`La)qN&aCUpxWva0F7w zc8P;~v;JLirFSV#e~R=5w1G*A3eg#fTF=ub{n)qfR;1TWhsROX_p^_GUlok7llF@!Q331Z>_&OCdgPoB^Wp#1KXg8@A>ll*4R*%8r+m%%NF?V~ph}C@ zQKH`yeAIBF71C@DCL6JBV}^xK3Qj9S86p15rv>RVK7Vrsbva|M^_v`7US`V1JQ2`d zfn{j8`M8Xgb{pf2q4(J!0LO((y6N>1<6JkiK(cFpYo@~3Z70?oweT$q7#DU~ddCCG zsjh{gN|z0VNMSD(GX{!y)Jto#kHxS)=9`}^Mq4G`ZXNQ^-T1Fdk?x)vdsGUzldktF z*gnDMjhOD-18Nym8+D^+B~I+>LTFz85|m;Ykwy3gn>eEn}E{@~bdv$oVtqK6k8eM5e>%fDf;c#X2+ zpyFD_TWGZ39)QZQ+eKSGYhL`r_uN&7;W$CB-f49NS$BF z^?eoB>aMM#qB7VMT<^4ZWPgkqBJd5d7fLxsW!P}hy5uLF?Ic4uDR_Uw)~5N+-6>u{ zG9yyp(ai*8KpKANENSOy>^t1wP0IkMmztpyPkT(1s5%YEEi&_7bZT=axmC&qN;o*u!OC4j)^*aPnS`le!CYK`OKI%pZW#ys9ezZvay3=`fsh7?6;V6 z7y)I@@cRzULSKf31zAxA8F~aGOobQX5*%i7!Cw%S!B$s;_LXiB>=JZ`b;UE-UdrUc zjl4NU!8@s9bFE;f^L3#GUZTx_^3>Ebxy zcPfN2KW7n+vaL;U2XsbdOq}z=`u^5;YnYw&XG*BZ7~*Vy!Pb(g9k zK+bnhZJb3=Wt-{pl+c~rsIA+xwH~CDjVq!ffA(%!e%XM73lrYZ_$QS>#DIBZU7`CP z4`Nf}x(*y8Lq78M^_?uyixszP6EQ1z<*C3WL@#+&MsL8um*NX=FrnldFc2Q1ptqlk zM#I#1b>tgP5EVuOF{D&*_Wg8soZi^3^V}Cf$m)(PaQS_UvEX0Jnzpim4+HNh7InLM zg4e{2Z?CNKg5YPF*^2PvSWD1N#26Fv4y@z=4;jYah9!i|BGmQtn|e~8?aVo)e)e+y z7aM?9C~Px?1M{H{oV^Bdp}?2Tvwxa`U=O#{$5tHOGsU*D})B`NNvUF(U1*4 zvtq%vw;*j&Hl>;k<+1u_qd8|3s@GGs_?{maZL5OW(TUT#UqzLjpDxus5X}C<3|0m9 zHj)G|i`mTeqE3izRA4U<)IRdc(A-uo_yP<9V4Eu+3bxS2N9 z3s&bUCO4{aD-PsU&3kt`P-8Jh&zYOOr9KE!x@NsUyBkpZr+8^!~OvG2w*L zOOnEF)`{;BN2ixkAExOzCdIa7bjSkqUMv*-Y3fO+xGp((Qpj`P2 z;H}r`Azx;h`}D68LS|nhZ-te`dsKWr-2V5}H^k=N`n-h9Klcr_KH$zPZRGOaKlCkh z#Y9kS%!#FV~0E*#5p zVpe+m=8tBC|L3M1o#tUbkNy7dZ;In@e3$xVh&7lM{0FfId3P|UsUx0AIL{}~qTv%f zF)UE_>VO_T=D?~jW{0pnCB-2t?=(-^GGaj%AC5U~Z4P9KJ&%nb%NX(yu`Gzyh+~UT zYdV1wkojpICiEZjSMfzL`lL(_zbhlq+B&%sl!Bsq`2?*#@~bA7(NfRe+r30y1&~_I z2PL6!o`~%g)j@D#Y8@?dx9cB}c*|DIu{A3C`f7=WgHbITGr;J@-ihh^+nZ?SAt=Vu zVLMqU*lNj_v=}bB4gcj{@BNMPvnOq@`2`QsqWpBZEyM>ra~p8I@Aa5#&6Mjy{{q`W zu1@uUI8oasr15HctM1TDW{pXzgO#4k$UMXQ}5=`ev%?PGd zJ)2joLM$jDNgF_g#&E$~|JUUHv%F7-1w2-V1=s{=st5F{(Acg{$o2a_pQi7%EDJ@5 zzXL|9M6>I4sk$JgpJk`ezoH;ho&QU7R>exo`&7uLCZK_>oRf0z3ID_}yu5^a5sbT> z3wuz_%e*X(ZROixBp+MVC!n-`8p2&Y$#L+oS5`bA^m|2oMw2%i3*9lgenl>F{T>xb zcL|&Ur*sOa^5$6_qU zvd_Qn>YiBe(mxFz15fs9JLEU-OW|Gn# zFxlD>4pdJV@_oPah!G>EaTCUDfpzohL6wX`*>&n1IjB(#Z8&IAIykFPb zaF>5^I&i9LjjzXlyu>yY%1%%qp_wp%CqQRuM4fNvZl%{6vgRK=(B0~4oq}g*3@6M@ z@$354=@M&tU2ErF`i~RFYTWI8l+1Ic39gB}1P^XwN0;gp=8+5{fow0B-q1>wK3To= zSbTM!5Qtg^Bn+^-ioS%Omt~zOSYjtLS?+er1Kh>PyWr)o<$qNPS-t;618+Z913n2& zUdqOGZWn~GI2)&ByMevZ{+@`&W1?MyWt#VnlwlzHy;~2b&BKmK%(7!SY9#pEes9jv zUwx2$C(`$heA$CZ^}wzI>Ijnb81m#2@$_v!g|vQ>XU|-wlgGjdk+4l7;63K6O|Rm92GZ$I$9G*mIB&Hj}TJ#1dnt5 zL~LX5yA_rWQoPwash%5pz>;O1L4Ep?3A*8jS);7mnGj%UzXdv zJb;zke_Dt#$a`QXA~>4ivyvu)`|jo>3FaPl-|t&-A!F}`Qd96b_DHMc3HLICMIHk9 zx;F&3nDD2XhLFD${Q{UGL>la}0eQpUysOKR={;hb`+uOoe=nhX8B+xsVw!)1M|fdT zG3wtg`tCj-H`1YwL?}yQNJ$-E*~pX`+r3#c6?|a;{9l&(IeYeP%@6Zs%){Fy@1z`f zrZx$<GS^8tzzHbT1hr#hyA-bd{v+b>07A&)jcIO zo8DR)a?)E3eWm!N+Ko$hM*r=vH^1+(cft3rXn7}-h47DyWXYjA`YmBEc6V6vx^hLV zD7U558!Y|kJ95||i~FVMf8aE8-f;g}wQP`+-?E`=%d~(rX5U{SF^fE|*MJ(U_XF^i zV~8utiPzJ)Uc}nR@M=3l?6DMv1bAO5*?3=T>qM0-!AW*SD1KkdkKZv94yp38hwH+n ztsmyz>Iihx92JR5ZY!SfoHAe{mBF>4bqxmuQ^v*1j+@$n?mRYd`iDK1OMC|zsXRq2 zTzj7HgBUH~G{m8wiq-pfKYF!A?|gLEgNhMeCENyfL*03`ADP*vBH^#X0gaxGfBqTK zx)ebX5016@cWI_{z_p#k3@y_dxuGu`=AN_@z^8T+S;J&BpG7JAX6Z*LLquh?O-}pB2!tO} zTUXUak%Mcga|Zwip9ZL0K>o8)&GBf9H7Y;OfzEIWh7p5>K3ZbF_7>!~uT(YaiUq^J zdv1-{$6Tr39Rnio4J?Ss1;6GI#bKWJLaDbVuXcVl{(kWv-b4#MuFTxFxr?{8P)_@Q z!{n#ESwcP-_fZb)^1|f9Gs0@TkBTccY4)^5sr3P^2_wu+nXe)zm2xuGcd9xzUqy+{KGw2f{g>AOeL zAdBJ*9=M7qX(tc!h|C2prQmgOAmZ&9^4 z9o92~vhPIcsl?`?bE`WUnw2qIyqvBBTF-+en$gg5L?h z4jt4TmKrw-c;`N)d^tC$?P(`~zMJ%zv8Hg%tUaKQ>6(kq){tKSD!ZoLrTM~n|mz~esX{j8u zL>SZs?Y6Wg*0k>le+}-qYjNs7D8*xegv6@B&vci);<8R+k@7zJ10Y!EN6yf6DW2N9 zvTQ)7DRb@MCS@BG?Jd5y(k^O<|AjoABtKJnH7|#DK12GP@8o!Wo4a8 zlH0h$E9nhoX9vFV%l>a`B7kd7D>coNP?xi-dp}_JeXbL2CZC^g^213R>HYx53ZC39 zc8!8Y4q=4Az=@e=qqgIiO9_W&aa!puc|h?5y|#NYW2c9}G4^SAiqNqC#1nBDso?c% zH1XJ&t<%S4s`iZSUc^4>idevqYGb)Y5cIOM~sB ziQB&O%$vu5(WWkWsG7ZvdN7oYk9JgKJohDC7qG9G^TMd?VmxHNVLu>qDwUT~#RS3M zn*(Qzdc^6wE~x?knKT%(|2#qwEZ4hEx~g}hw@>c#i<-qbH$RD7fSmlK*uQkegl|T< zBKW0|TYvn2mTdWL5Au^Y+O1|Qb2#=<)&CoKANNSS4{ojj>xE=q(Js*`Y|6jTgmZ}J z(IGXn?p?#Sujn46E4uSl;T+dGyTEO_6VpkXasV`N9mlX+y_+OAcaT2);r%s%LXU%) z0z?7EeBXy;Tms*GKjwLC>C+UOvByIRa&eNXxy!w+zq3!5&u|OQ%1(wK3`J?nNY=v!fDLmG~K{y#z_(MOs5MQT}#oyzSdJ&Z7wcq;{%DYP{jWoIc zqP_j~%3vqauBXeB;l4uQ#%nwX!)VrC71jT8Y1uJxqq*9h1&(Wg(jbbvZRfZ=hTxu7 z&%qmwP217rE-}dQ{Z~!Kq>@gFB>oS?*PI5{OvX$hAF8?uj(+7q%y2x9gPLp%E}E=z zDY9)0s>;Z~BfQm5eBzeu_~z=nu4|5FD6qC}KsnWpSh*JcvxgyKj2XPKiU5Kz{)kv9J+oTk^J@gEsr7WQS2O4;IkR+Q6E!2cJc zzN_7pl<8UN-^A@XopSlt z{}&Lv<-c|?U;2k@f`(e=MS%cRw_88y_vEZ9(mCiu1mq0%+e-}Ze`u08p1zkgI(^=bprYd z^-Tu;4^c#gf4n`kyvK#>mmdoJS5eC;6>ABbO5C#jM=t~jXiEG)?7ewBlzsa@Yzak{ zijX}Ok|kwdMkFNrTGo`1?E5xD*2un9c3Bb`Wnaeojr)ZjU&iQfRLoC_Zv3kLM z*H;zpuT_#c1F6A`hX1ar-CG)W@^nhIrT)u76EC7TIyMj<3|4ihv!!6|I>OSxZ9<1W zk|%Ch4 zV|&_nc=be+fN7RZ2BzD#L+e3}nwTnd4scSs_1cKyH{^e$x$NGiCgRtSjsVK_aN0oK zrB54U?O7O%odut~(88t$bF1p;0Xo>y0z|;-)MzFt9h>ZFM4u%MFizD0UYK4c*9j?o zcFQ)TQPnSaj@v=B*#*wa%U>C5JenoA3x8#Zp@+Fw83cW&dEemN%31DZ3on-dO;TK9 znJE*#3vcIQ5_W48=)}DK~;1T#h?&VgemwiniT(X&R?>_UPywR{q(?^~xj_FYDA%=0iCgzLx zVNoRR!7-jvjP4b$MK{vo?ctP6&m27lW@8I-AlAa;`g~gxm6)FEk|UxkR4-u*PvtKn zjD2^p*+wAox7)=?-UIcqo!LwDxRJ7266@(8ylOF(W<$93)b-{AwiEOTx{Pi21RBA51PQxD+PB1m#- z8X%VUz1u4!T6x0Zn5kC26cLs4&9rCNvV9tW!WUw{po?)_Y<>?7TTElY0*rldTxGp( zcX=?I#QW#HsckmS8gp@E;)bhJR0 z?VF(28=1P}`|}-xE`N761%sR$=&V# z^1T2h&)mz!Y!NL#EL-rRs~y7Tu_pTZ(Q(vD-(?QOu?W%0r62Y%a!P+^?}y87Pi3~U zJ1_P9Y310{WjqiJ`M7`QwV*@jT$u!4Q4S$l^-Ld!I1pSMoLkHmQ;fB% z3`#zR7rn`UE@G7%P?Y;&V1(L26EM!(&1ReRDIds-CUAlGXDNY5#QRNM_rn)C{}V0} zab9?Rya87#hhGV>@?@$lF1~+>a}N2TWXYOfEpv;W8{N-Ycfe#@&9YIZ;ob{p#jdU( zR}W(gIBQD}$}$CxIpRJd0mX*Fv-gJ6F_F-u*?WQpPv-u=15%IiiD27|d%14x-`e+< z9*V=rkGepzVMLSFvzD0zh5cq#SM;{8RejnkI5NS{&S^@|88anzJv-H41&;W{EG@eC zh;izOT`NvE{=O@AtGhQaV%!^H(K)xDeWI2pl(82WC$VXv4_l zkH-$uCklJpr2tZuBfmS7R&WnU3&^vA%$%idTO;msrQouG`_RdOD9NpnV($&N+V27$ zIkyQIulTUjxV>juzakH99XsK>zPJXt?V?^-emKJHmTwFI3U8iE-K!?Q)!K)7in_;g zrDguchb`Xy)*?MS%`j7+gyB5})vwPES#1NY4%{}jq@L_-fXbXPcdgLpnOaHu zM4n8#U~H>J4EHpA?er!UzhUfl1*yG@#MYqAM`5FE3={=my+coiJRsXy1#vYm zk>t&p#lFNGdyQstW=Ii$h1$1$M?d>zNbV8d7ZEO}U{=?UA@0jK{(g{}Ud8FN>bz07~ z4~ubl;i(&a0hOvLC7UR6g#&4Cr}eMG)X|d7 zV@36jHcQe+0l%GVE$c15*&t7dljSA%Pq*FDyhn#G*>W8{cjzblwB%=J@}5cTDwN}D zU~Ol9+f)vz1M}4GWDG#%4ykQ*F8W|MNankle>0Tlm3{LGkV78UxNPbUXJ2ADa_q>` zlZh7%&%+#+=3%G@yqX?vl*CG}%KgC?oTIb`RNzc##c!jl=`5 zdO;8I)tLG2vyV( zAdLAs$m<1FHNk=pD`~7lap;1upJDT7WF=s4J&+pDxb?wyGx}2|5=y#OV?921Xf4fu zt_bmO^+_4Q@n+cjtAle>*FNzBHfH>*sKr_j(P=$mTU6zGU|33Om(8`D!_}Ql;OLll z1Z{D@wfs1iZ>!gkbzh48h+M%IJtNY`RIo~mBC;{-WGL2T?zMKB;|~(zMndL0 zwjJAR5T~jFINE~Zv#T+f^Qo+}-ifB`V!126l_oT`5*%h6{VISYrw1mAraPW}z*2Ij zwp8_Qj?2{8yQQV5#gx1*JePH{6a0@bKp*|UT3+;te5DM#FaIs)jMK_!Win7(=To$O z%4nE#w9u`>)^JF*oYP|>OzLy15?9*M}>g3Ka;k*$F(ct)uX=ZGB>_5K(zE2~fXKL3<#_2ink7MX2oF!EN;o z!bzY>L2DwnQ{0)aWU5hG(}pianPg!<63<*(EVQf=2wG};b!gy#v3Vup(%!l6zjW&} zVztR#)O62zf7~D?#{)KQ8|ZmLFBQoI6?nIqdQM*IW9dgm*?Z@^44u{hG7#9nn6PVa zCNvNo0JFE`Sc#Zwv7OOPP*PksQol(F0)EU`uZQDrnV^9IRx87^mnnxQT_6^)vg6WW zy*2-(Hin_(mg#$?vsu^690qQdy&Oma5GV0qZY3+ypZ)~Z@Le9qm3MHF)wmUYvaF8LMK1WML&%p^p+%Q0oz94Ss4!(QspAwyTC;3Gj$SPMo{;4hO$ zG{3yoF4R7+a^agkvR(J?=8Gl)qHHB!KMJYd zFORFnd2aDm?(H>2g%cXGAU7p**&^iQSuCZrD>X?rRHN15Kvgs-eKwq9{0hf7bF;D` z-e|5!fQSBwjcz9!o=kSRV)Ut^kX$e6bCWF(6zRPnU$$>qwQl$BoQHPLv*AwT zuLnS7o-G|p`n+q1b`Guov(+FG_#H;NcJ5m0Z4)c7^$<;TvpB-shB|UU5l7+P1rXiG zx_BRzqbN*UL}DNCd|$O0T0fZGxGIP+2}7xmBT9$RhcY5YGb(%6VPI*yCK`VElP(zI zdz5l$zDCdvzyVMyRV&pPr5EI5Iny|&3zu(Hig4@tT9rsr7pQdzjwZ3dp9!M+IRs_?O8it_0o8-!J_ z1pS!j!BpPd93$*(!m9XkmK-+@%N{4q*9Q>u6#XUa#MfaAqFj{LeQ8mAthA!)@HqAk zbtK_s=4qi-soc7k_1SeZoNIoBh^dudCmFt^)zG6GsVTI43zBbw`bm2(iOLS#R&zKGMUvPyd-6(zC5(B)lIgh#F9!rJWl) zVi!qIjGg=D@PR;?v+(4Yq-6l++O;WNzRq6;y#0HNO-8EmHjV+^*MW>EEK53>b5 zTCuJ|xlhBB*ZHx_rDs9snFi#`NC%8A#5XIf&*H;2C3eI1lqn}LT>zAb%1uVh?QlE; zQ;Bkdy-nv2hj}E}`XbIXR+N(f0Aaei1;4rB;PC6cEy$lU(r$+W@!PO~Bm0LxU9n{9 zC8y6?ndABXj(N|W!G2bdc#($nO-+uBCw}Z|Hw<`n@UZfEP^!f5V788DzS{t(SQ7^M1TZ zqdjJF1|)KVy0QW^_C}{?5;AGGA0r;4C7^0nvLuj{wC%^Kpx}`|srAN5$B2Ei+69&_|K@mF4h@{#a>?)UUB4e-siwVeIyH9^U36 z!a#Ri$k-uBx8c&UtjFFG`}LwlE^2wkM+@IZk9^q>nNQ^eK+fZ*GL*=#TRS=`QX zw%I9Q8j*-g0$r>(H3Ma+ghVfCU4%#?Y=pG#qXlt$->Gi|UF2uZ?i12F%!D*55!v3} zZbPn2V1#%I0aV#DmoK~M=7u)`-qcr$X0r7R36n!+M9niaZ&^79lCD?S>n+;WCvG8q zduZLfV`V8+$xN&Ew}x1@LNICo|H%TyFj$miOD_h`sa{Q#f_p%Lu z8#&YbNP({+D`ILrDsu|BmCQ*Wy#p#bo|~m$*foFGiL`s59caPu9hBYvkrF-fg;LkK zg_nbf!ml&wp$wU^N&GHluK zfkmGGC8(LLh`r`bWp>v~LZ5N*GA8P;oy2Ln)a39PpGIc?`7VsPmt?plQ-nLBBGS3k zRDCO$LT%hTZX&L(bY7xxV6dqj`U3U2Z1S!ljN@c{e<`OTi`)tcJg0F+$k=PNuEZdl zhGI|lQZ?z?;(!&vZ-;L6_zmlA+nKEYtbfE+7XuUSSxn>BDR9~(t3oaY4A{^dm3aFpV;SRaiLa6{1vefaW(sRu(qH&T2_yVh}-&jQi>PH;(v@Kvu(NHEKgAxSk4w zS=(!p1OOfPvZqI45YZ^an!_r#YMwZvX89KRbC|MkJ@sT_Hi?bYt%AcUtDWjZV}mVW zt#wlw1N?V3XZL8?=PzO0dR}^o8d;^$Hh8&%2R;zOjBh_YRNpE@=CbuYV_&{%Rg%7j zUPol+p7S1+l$U=)2ASSX5*gx>#)x!IpC|$k@I+Qe8u!FW z4KkI~Fi~J;72D4_PniZ%{*EbDF0O*WC(QWFB5(|A7ceusXrRut6 z*#XsuS=HOd_ZzGeJ|ikbJ4{5gOute#2M|N#q9RoxtSSr| zSq|HP?)uaxBV^p}FmhurZ==4^JirZ&@};pzS0$xAda61lprV6TF#o|JtvCeTN@EMj z$igWD)G5}6>!**y?MaS|n?LqXY@%YAjTaqf2v0svE#T|aGJd3#ucD4q!f6n$LhKq3 z_oCk#|4Owp8kLZj;$gix!}ll#manJu@v5K)X#&|VJEkU%Ztszgh)C5LHt(4@jMf=m zqAb)>f>u&p6B1$A5|N~(u)-!==x{uSxmQD95_Iq&GkRV#K~oHFtL_2fk??z?AC){_ zK6^_H&FManrL2m85V-(a!JWXmIGIe^mFHI!v8qcIt1LKH;}FE+yI_$(paeCv|1OVg zZekzn*-0_F*!MHLZkG#}Vibr>BU6Y_MQemR%+px?_&3?XUK5|Kp$!tLLvPzl%SBdK zs&<%N>wJTD&z*?7AjP8B_$M^`w<>Zt;|>ICzi?^{BtfcL&)OZn$*&fftWtEV9_1Fit^892Rgf z5XdmpvZZ_W#NdbG2>xW_5Wc?e%h`2XH+?Rt^cVi=5OI6W(v_4>!&?ousDk*v-_5ig z9gONIN6fS8=1}lx#or3fq+&<`%pF&72ww*r! zhxDW~CzXz)_2ooggZu0Rd&7)zKY|Nc`1F#p!=tW|9JxDrL0sLj1e=>xNE;8>6QO1I zwF1!w-x^HL@fy)eW)+kuid@vIh7Es-`z5Hs}cXt_Pi*x&MiGw}FReP{AcRn=;>*>eZTMy&j9wHX0_ zjo|(mH=OzL(_7E})Foi--B)K-!5de(|Eo&Z}Ax)Hs!>%8|_n|mzKc?mfpm^U>7 zmcLijOnh}l#w8)ESYDo>A>;LoOIY-=NJz91U3<=uOLeb5PV=v$kd*M7zC5Mx zaOB^-JibIBUq~K^D=149JSl1WvV(?Oh1@T)!`;k78ojiLt+l=V-d+t$#9vbwh<@unXzH5c@kgYQbY>C8XjA;i=qH>-t zj_N4Tg)^0RUAy?=+KBVg*kXmC&2>rFvJUEZ&P4GGkJnRbLT!96D?b^j5^nWHU68pl zNwO?8o?N(1JNA@PqRsk=1ZfIrZ|Wv3A4ZXe2ZoEKjE7w-wa&s=ZQc|-uZ;rvs-T~# zv7xZB!1l3tXWj7GGbX(AbAI7#-1ulxNi%=llB6VRJx>8UsOH{b&h zG4=#&rJh_u#^;D98E%!h%{A)`fr>fZj-HO>fxLye&Lor~4YzKf!+ygEs2u0iG;w!N zO_eANvN^LpGl9HM;7zC?&J9Qtnsd~Af3DXJ6@a9s!(qpwk4*rR1`eJxw;+8~OYlo6 zi_`h zK|0So2v(sROJv2*`^@j3`ju>H2&qZ+10#fC>w8ws`iU^F$j@2*rU!z zu#ZeH5aG}G(?}1$=%fDO^V&A1~jLHY)0~Rq}o@&qDhQ(17G<5 zs_Ks#);#dg9>^|i8WgOogDBKGN`N$@HhH#@v-L|{DK*Peck#ozmwMXx%UlYeo=)vR zwmf}nIG+Nn`ldG)ji&{ouNPoy+|lE`R<=)`7-bBIFR*+Aw6TwT={50C)SnH*+Gm}v zm_7Guv-46+?(}zadWfxylk~iSMp0TsDCPMdmYk%II(th5VKeoAqjaAI;Y}0r*3p%B z7J4pIzQ2DmoAhjX>4Nbn#|KTs?AO5m8l1Cix@i-U&s=R>Xml3s>nm|}K=MzEOi?JVoY*qu%2p?KRTCtY9ura_vu;qH{3|v@V zf|bHYsCwLKWo8@foM6L-I>fHCH-V)|@DX|Sm1!|^x$@fXzr=^|7p_5nW&!-S zLc|8!zlDgxr$WRCzNdXIX5mBJTu|N$7hxgBDl>EM%tJ85r=a$32L_$b2|k9NJr`UP zSU}p7=x0Q~f--aCm29{pvVt}67MQiUZmoR~!o4&@{@ zK?X2`=jkWQ!eh!;)wa(?8_3NS@l#lmwL4#y5Gtn$k>p@S~z9M%5+=0iZJ(TMc-`zJpHAjy~PmLY+n|Ex$^4# ztV?wo>ziIOBJGM9tt6w-(W%PjTnV{{Z)M9ZlAnA7o#0IHEP zsNk#oOQa^TT)p>}>EL~z0(Qr6lB@#FT`waC8R#Ui0h@VyzcyY9Ok3CEMr? zRLn5gyZTcK2;XHo#4?=qt|KoYc#;mj>=J(8;*$4w+Yjt4r%So74kC2`3UrFp5&R2M z_gF10fBX0FS^ZOnqH&V5Z~g*G^r6P@Q~q>sop20}lhUT#Gja~uuok;vuHYtuAQ)hob~;BHBN z*N)%IAr;%QvS$eh_#$Yl6MD&B)K>qEujT(=<7>(|e2pG`PjRbdtsifP&xdwT){WR1 zCXK*T`NZVuyxj7M@_fjWTks9@J(GL}(}#K|2E8;bLs%EL>{)Lj4RwQ97&Iq&Bsh`l)zRp6b$c&smbdMn6y&sp>FPm` zKjnBqA`Zc$0#)=-Pu)&*Rw3wC{`|?0MYVfBAM8aqH@_NVS9_DuOIn=x_0^Lsmss{R zuU4M8vd1dkk)!q0DSVd}*K>NA|fz0My^GCBt__g~&BYQ;`^ZdNyZ z+6_4IH&r!g>BA~cadJ>?6LhgRvqL1F>b0>NrmMfc`v`2e1{mhR-ANk`2gr+>awfeT zz`9i+({Atf1@}LieCrdv*PrNop#bCONS_mZ(zAB=*aaB36V!5BMX5pz7$XYGuYOP> zsah`8I~`htu{R!uzjOI@8FRd!5d^bt**;70?#r(`vlmYKIo#R3KTP*8RqcLcl~R47 zxl0%>ZD-9QB=3G$zI^UzO2&VKvTMbP_xjr25TL0s2W!}L-B(VQ;V#Sd!hW{!^Ft7M z>!Df8fK7@PnkFDonRv>&XbNTl)|_>ow{5=>@E;Y@_|!0CTYw|U84+Ocwmo0l-Y#HNV zGi3LVPKQh{Om8(%9VI+iJ&kscnHpwa+XlTVe}wnw$XYGjs~dGm6~l)jHf8k!XH)lY z%i?-g0<-I!ZmN}%oTfqxKY79qo7e@H0?60FMtfkUlJKA8i9dzi20Xsv5iH$C5okYq zgO<9g2bdb5Edkdt55R9vSBvQT%s000rSe?~i(%Rk|DWL7WdMA0|L?&!6VjiR1RhR3 z9>Gi~!Tg5d8~U!|`3qi5Y;x-tY~F>FRR+`v%IN!p&>^@tG5L5C`lU2J92G>PJmPS? zpYoxiUW!>`%Rj6B6$B%{S!aH`n#Pp>z9;Z~TBzBmRKIO|aJ?Gi9o|>0G29$-X(Ci! z2wgr)S*ETw@s1f{k}B27!(jU{;KkgNeY^dbGWHwGiva}tGbT2#m8_W$zi&0y6c|w! z(<-Z3#%W41Vc`3O`{%%}AUhe1mKVZ#{$6@D%p}Gr1p9PGM@j$0Km+tFJ!@JAD|=Q4 zYa(|O9DK75F9>{Mf}WT2b@H-EU{76dSngL@u-e4U~gnZP)e%tqtZ>ll*BQ{S?K$Koik}gbk~zV&apyFL{zEKHG#S{(>LW4}PfI zoiLR#<9r-ut#Bx^b{fH(npl08D2h952YOZ3H3`q z22sz#F>WdmK(BOe3R1m^B}}*$zpjFC=NmY^^}7eXif8XUMvo)9G1!IGX@wMig*htp>P^2!y zOKIZr2#HT-NY@y>buVWsth^Hc~TY z7gY?wEcHr(`OWUyPcjOl)p2Ah@r)P22{3a0~<;?lr9Qg%r;!+unPfk@BN$%LwVgi0!M?aCT#kiCc zr}6;W&F5od+-b4W%= z07Jx?gbgQ#+Q=#7s0tFkE~A6YqW!4lVNID@p5r!+gK$oJBYD@Kd#y_fc!x-x7xR+U zg5JPY#`CGOKRgw6z$1xk9`4Bz2M3G1VP}}>yG)Cir~PE44X9&*{qklH30Smx)xk^o zT*d`@QNTQ|?F+`YWn5u zeA7)SgXabq6hw;AGccAjTsPh4gZ#by;+$Qoi+ckzbiqDf@5N5MDQaQHHzRs>bb5N7 zfYZwjFruVNNpbBeCQ%~J7|Vvhw>mNHdm)127fw5nZ^YKwf?=yCv^`3nFHaXR@48p; zrzdAq&&T%CCR71?wK_xReV^qi*U6lS!vpAmNE`nB6M|HUenDK*+E#f!#k}BN6lW8z zqykcqYO!xYMv0}W%v}j|A1}$&r{lY6dp_jgVn9tb+odDkd<|`2+6Fqtuz&<6R#WWd z>tE2P!h#f%B~sU@3{ls;{gqw`1WN9YS_;x}C%BnK0|zo=o)nGnS(+mtCczOWg6)!bSHM4)dqg(?mgH~MQq z85c)_(+`+k6;{|NslY2&nvQsN|+#-{#~_WF7?^fNj_i zsie+ihwX)qzEEZXn_ZfSJD@x+WClg=GK7!f-@*~e#eOXBrjOqqU8L(;MeS)zK~G#1 z6Q)�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