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 347bef63a17..d3f3711ab58 100644 --- a/src/contents/docs/05.workflow-components/05.inputs/index.md +++ b/src/contents/docs/05.workflow-components/05.inputs/index.md @@ -175,6 +175,7 @@ Here is the list of supported data types: - `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. - `ARRAY`: Must be a valid JSON array or a YAML list. The `itemType` property is required to ensure validation of the type of the array items. +- `FORM`: Groups related inputs under a shared `displayName` and `description`. When a flow contains at least one FORM input, the Execute modal renders a multi-step wizard — one step per FORM group plus any ungrouped inputs, then a recap. Children are referenced as `{{ inputs.. }}`. FORM inputs cannot be nested and do not support `defaults` or `prefill`. All `FILE` inputs are automatically uploaded to Kestra's [internal storage](../../08.architecture/data-components/index.md#internal-storage) and accessible to all tasks. After the upload, the input variable will contain a fully qualified URL of the form `kestra:///.../.../` that will be automatically managed by Kestra and can be used as-is within any task. @@ -306,6 +307,114 @@ tasks: You can access the first input value using `{{ inputs.nested.string }}`. This provides type validation for nested inputs without resorting to raw JSON (JSON inputs are passed as strings). +## FORM inputs + +Use a `FORM` input to group related inputs under a shared label and description. When a flow has at least one FORM input, the Execute modal renders a multi-step wizard: one step per FORM group, a step for any ungrouped inputs, then a recap. Apps using `CreateExecutionForm` render the same wizard automatically. + +```yaml +id: provision_environment +namespace: company.team + +inputs: + - id: requester + type: STRING + required: true + description: Name or team submitting this request. + + - id: environment + type: FORM + displayName: Environment setup + description: Where the environment runs and what size it needs. + inputs: + - id: region + type: SELECT + required: true + defaults: eu-central-1 + values: + - eu-central-1 + - eu-west-1 + - us-east-1 + + - id: instance_type + type: SELECT + required: true + defaults: t3.medium + values: + - t3.medium + - t3.large + - t3.xlarge + + - id: notifications + type: FORM + displayName: Notifications + description: Where to send status updates. + inputs: + - id: slack_channel + type: STRING + defaults: "#platform-ops" + + - id: notify_on_failure + type: BOOL + defaults: true + +tasks: + - id: log_request + type: io.kestra.plugin.core.log.Log + message: | + Requested by: {{ inputs.requester }} + Region: {{ inputs.environment.region }} + Instance: {{ inputs.environment.instance_type }} + Slack: {{ inputs.notifications.slack_channel }} +``` + +Children are accessed via `{{ inputs.. }}`. In the example above, `region` inside the `environment` FORM is `{{ inputs.environment.region }}`. + +### dependsOn across FORM children + +To make one FORM child depend on another, use the full dotted path in `dependsOn`: + +```yaml +inputs: + - id: cloud + type: FORM + displayName: Cloud configuration + inputs: + - id: provider + type: SELECT + values: [AWS, GCP, Azure] + + - id: region + type: SELECT + dependsOn: + inputs: + - cloud.provider + condition: "{{ inputs.cloud.provider == 'AWS' }}" + values: + - us-east-1 + - eu-west-1 +``` + +### Constraints + +:::alert{type="warning"} +- A FORM cannot contain another FORM — grouping is limited to one level. +- A FORM cannot have `defaults` or `prefill` — those properties belong on the individual child inputs. +::: + +### API submission + +When triggering a flow with FORM inputs via the API, use flat dotted field names in the multipart form data. Kestra maps them to the nested execution context automatically. + +```bash +curl -X POST "http://localhost:8080/api/v1/main/executions/company.team/provision_environment" \ + -H "Content-Type: multipart/form-data" \ + -F "requester=platform-team" \ + -F "environment.region=eu-central-1" \ + -F "environment.instance_type=t3.large" \ + -F "notifications.slack_channel=#ops" \ + -F "notifications.notify_on_failure=true" +``` + ## Array inputs Array inputs are used to pass a list of values to a flow. The `itemType` property is required to ensure validation of the type of the array items. @@ -624,6 +733,19 @@ tasks: When using `http()` inside an `expression` with secrets in headers (e.g., an authenticated API request), use named arguments and string concatenation ([Pebble Literals](https://pebbletemplates.io/wiki/guide/basic-usage/#literals)). The key to the syntax is to use string interpolation with `~`. ::: +### Dynamic inputs from a subflow + +For cases that require complex logic — running a script, calling a CLI command, or executing multi-step tasks — use the `subflow()` Pebble function in the `expression:` property. `subflow()` runs a flow synchronously at form render time and populates the dropdown from its outputs: + +```yaml +inputs: + - id: region + type: SELECT + expression: "{{ subflow(namespace='company.ops', id='fetch_regions').outputs.region_list }}" +``` + +See [Populate a dropdown from a subflow](../../15.how-to-guides/dynamic-inputs/index.md#populate-a-dropdown-from-a-subflow) for a full example and constraints. + ## Conditional inputs for interactive workflows You can set up inputs that depend on other inputs, letting further inputs be conditionally displayed based on user choices. This is useful for use cases such as approval workflows or dynamic resource provisioning. 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 b23841f1396..25dfd35432f 100644 --- a/src/contents/docs/07.enterprise/04.scalability/apps/index.md +++ b/src/contents/docs/07.enterprise/04.scalability/apps/index.md @@ -389,6 +389,10 @@ By combining different blocks, you can create a custom UI that guides users thro | `Markdown` | OPEN, CREATED, RUNNING, PAUSE, RESUME, SUCCESS, FAILURE, FALLBACK | - `content` | `- type: io.kestra.plugin.ee.apps.core.blocks.Markdown`
    `content: "## Please validate the request. Inspect the logs and outputs below. Then, approve or reject the request."` | | `RedirectTo` | OPEN, CREATED, RUNNING, PAUSE, RESUME, SUCCESS, FAILURE, ERROR, FALLBACK | - `url`: redirect URL
- `delay`: delay in seconds | `- type: io.kestra.plugin.ee.apps.core.blocks.RedirectTo`
    `url: "https://kestra.io/docs"`
    `delay: "PT60S"` | | `CreateExecutionForm` | OPEN | None | `- type: io.kestra.plugin.ee.apps.execution.blocks.CreateExecutionForm` | + +:::alert{type="info"} +When the flow uses [`FORM` inputs](../../../05.workflow-components/05.inputs/index.md#form-inputs), `CreateExecutionForm` renders a multi-step Next/Back wizard — one step per FORM group, a step for ungrouped inputs, then a recap. No additional App configuration is required; the wizard is driven entirely by the flow's input definition. +::: | `ResumeExecutionForm` | PAUSE | None | `- type: io.kestra.plugin.ee.apps.execution.blocks.ResumeExecutionForm` | | `CreateExecutionButton` | OPEN | - `text`
- `style`: DEFAULT, SUCCESS, DANGER, INFO
- `size`: SMALL, MEDIUM, LARGE | `- type: io.kestra.plugin.ee.apps.execution.blocks.CreateExecutionButton`
    `text: "Submit"`
    `style: "SUCCESS"`
    `size: "MEDIUM"` | | `CancelExecutionButton` | CREATED, RUNNING, PAUSE | - `text`
- `style`: DEFAULT, SUCCESS, DANGER, INFO
- `size`: SMALL, MEDIUM, LARGE | `- type: io.kestra.plugin.ee.apps.execution.blocks.CancelExecutionButton`
    `text: "Reject"`
    `style: "DANGER"`
    `size: "SMALL"` | diff --git a/src/contents/docs/15.how-to-guides/dynamic-inputs/index.md b/src/contents/docs/15.how-to-guides/dynamic-inputs/index.md index e0031dbae3f..6c2c046fdf4 100644 --- a/src/contents/docs/15.how-to-guides/dynamic-inputs/index.md +++ b/src/contents/docs/15.how-to-guides/dynamic-inputs/index.md @@ -131,3 +131,77 @@ tasks: :::alert{type="info"} When using `http()` inside an `expression` with secrets in headers (e.g., an authenticated API request), use named arguments and string concatenation ([Pebble Literals](https://pebbletemplates.io/wiki/guide/basic-usage/#literals)). The key to the syntax is to use string interpolation with `~`. ::: + +## Populate a dropdown from a subflow + +When `kv()` and `http()` are not enough — for example, when you need to run a script task, call a CLI command (`aws ec2 describe-instances`, `gcloud projects list`), or execute complex multi-step logic — use the `subflow()` Pebble function. + +`subflow()` runs a subflow synchronously at form render time and exposes its flow-level outputs as the dropdown values. The main flow does not start until the subflow finishes and the form is submitted. + +**Step 1 — Create the data-fetching subflow.** This flow queries your infrastructure and returns a list as a flow-level output: + +```yaml +id: fetch_aws_regions +namespace: company.ops + +tasks: + - id: get_regions + type: io.kestra.plugin.scripts.shell.Commands + taskRunner: + type: io.kestra.plugin.core.runner.Process + commands: + - | + regions=$(aws ec2 describe-regions --query 'Regions[].RegionName' --output json) + echo "::$(printf '{"outputs":{"regions":%s}}' "$regions")::" + +outputs: + - id: regions + type: JSON + value: "{{ outputs.get_regions.vars.regions }}" +``` + +The `::{"outputs":{"key":"value"}}::` line is Kestra's [script output format](../../16.scripts/06.outputs-metrics/index.md) — it's how `shell.Commands` tasks publish named values that downstream expressions can reference via `outputs..vars.`. + +**Step 2 — Reference it from a SELECT input in your main flow:** + +```yaml +id: deploy_to_region +namespace: company.ops + +inputs: + - id: region + type: SELECT + displayName: AWS Region + expression: "{{ subflow(namespace='company.ops', id='fetch_aws_regions').outputs.regions }}" + +tasks: + - id: deploy + type: io.kestra.plugin.core.log.Log + message: "Deploying to {{ inputs.region }}" +``` + +When a user opens the Execute form, Kestra runs `fetch_aws_regions` synchronously and populates the dropdown from its output. + +### Chaining dropdowns with `dependsOn` + +You can chain dropdowns so the second list depends on the first selection: + +```yaml +inputs: + - id: environment + type: SELECT + expression: "{{ subflow(namespace='company.ops', id='fetch_environments').outputs.envs }}" + + - id: cluster + type: SELECT + dependsOn: + inputs: + - environment + expression: "{{ subflow(namespace='company.ops', id='fetch_clusters', inputs={'env': inputs.environment}).outputs.clusters }}" +``` + +**Constraints to be aware of:** + +- `subflow()` is only valid in the `expression:` property of a `SELECT` or `MULTISELECT` input. It throws if used in a task or trigger property. +- The subflow must complete within the timeout (default `PT1M`, max `PT5M`). Keep data-fetching subflows fast. +- Recursion is capped at depth 3. 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 822a9dd383e..f885204f1dc 100644 --- a/src/contents/docs/configuration/04.plugins-and-execution/index.md +++ b/src/contents/docs/configuration/04.plugins-and-execution/index.md @@ -266,6 +266,27 @@ Relevant runtime-wide settings include: Those settings are documented in more detail on [Runtime and Storage](../02.runtime-and-storage/index.md), since they affect the whole instance and not just plugin behavior. +### Subflow function configuration + +The `subflow()` Pebble function, used to populate `SELECT` and `MULTISELECT` input dropdowns at form render time, has three configurable limits. All three accept ISO 8601 duration strings or integers. + +```yaml +kestra: + pebble: + subflow-function: + default-timeout: PT1M # timeout when the caller omits the timeout argument + max-timeout: PT5M # hard cap — larger values are rejected at runtime + max-depth: 3 # maximum nesting depth of subflow() calls on one render thread +``` + +| Key | Default | Description | +|---|---|---| +| `kestra.pebble.subflow-function.default-timeout` | `PT1M` | Applied when the `timeout` argument is not passed. Keep this short — the call blocks the Execute form render. | +| `kestra.pebble.subflow-function.max-timeout` | `PT5M` | Hard cap. A `timeout` argument larger than this value is rejected at runtime with an error. | +| `kestra.pebble.subflow-function.max-depth` | `3` | Guards against runaway recursion. A subflow whose own inputs also call `subflow()` counts against this limit. | + +Increase `max-timeout` only if your data-fetching subflows genuinely need longer — long form renders degrade user experience. Increase `max-depth` only if you have intentionally nested multi-level dependent dropdowns. + ## Related docs - Flow-level plugin defaults: [Plugin Defaults](../../05.workflow-components/09.plugin-defaults/index.md) 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..4e12492d093 100644 --- a/src/contents/docs/expressions/04.functions/04.workflow/index.mdx +++ b/src/contents/docs/expressions/04.functions/04.workflow/index.mdx @@ -54,6 +54,53 @@ Retrieves the output of a parent task. The optional `index` argument specifies w {{ parentOutput(1) }} ``` +## `subflow()` + +Synchronously runs a subflow and returns its terminal execution result, so you can read the subflow's outputs, state, or labels from within an expression. + +```twig +{{ subflow(namespace='company.team', id='my_subflow', inputs={'key': 'value'}).outputs.my_output }} +``` + +**Arguments:** + +| Argument | Required | Description | +|---|---|---| +| `namespace` | Yes | Namespace of the subflow to run | +| `id` | Yes | Flow ID of the subflow to run | +| `inputs` | No | Map of inputs to pass to the subflow | +| `revision` | No | Specific revision to run; defaults to latest | +| `labels` | No | Labels to attach to the subflow execution | +| `timeout` | No | ISO 8601 duration; defaults to `PT1M`, hard cap `PT5M` | + +**Return value** — an object with four fields: + +| Field | Type | Description | +|---|---|---| +| `.id` | string | Execution ID of the subflow run | +| `.state` | string | Terminal state name, e.g. `SUCCESS`, `FAILED` | +| `.outputs.` | any | Flow-level outputs declared in the subflow's `outputs:` block | +| `.labels.` | string | Execution labels as a key → value map | + +**Primary use case** — populating a `SELECT` or `MULTISELECT` input's `expression:` at form render time: + +```yaml +inputs: + - id: datacenter + type: SELECT + expression: "{{ subflow(namespace='company.ops', id='fetch_datacenters').outputs.datacenter_list }}" +``` + +When a user opens the Execute form, Kestra runs the subflow synchronously, reads its output, and populates the dropdown before the main flow begins. + +**Important constraints:** + +- Only valid in an input `expression:` context. Using `subflow()` inside a task or trigger property throws an error, because blocking a worker thread while waiting for a child execution can deadlock a worker under load. +- Only available on `WEBSERVER` and `STANDALONE` server types. The function is not registered on other server types. +- The default timeout is `PT1M`. The hard cap is `PT5M` — passing a larger `timeout` value is rejected at runtime. Both limits are configurable; see [configuration reference](../../../configuration/04.plugins-and-execution/index.md#subflow-function-configuration). +- Subflow recursion depth is capped at 3. A subflow whose own inputs call `subflow()` counts against this limit. +- Executions triggered by `subflow()` carry a `system.from: subflow` label automatically. + ## `appLink()` Enterprise Edition's `appLink()` builds links back to Kestra Apps: