Control Ansible Output Exposure with outputsMode EXPLICIT

For the complete documentation index, see llms.txt. For a full content snapshot, see llms-full.txt. Append .md to any kestra.io/docs/* URL for plain Markdown.

By default, the AnsibleCLI task captures the full result payload from every Ansible module on every host, including module_args and raw return values. If your playbooks handle credentials, connection strings, or host-specific secrets, that raw output lands in Kestra’s internal storage where it is visible to anyone with execution access.

Setting outputsMode: EXPLICIT changes this behavior. Every per-host result in playbooks is reduced to a status flag (changed: true/false, plus msg for failed or unreachable hosts), preserving play-level observability without leaking host data. Only the values you declare with the bundled kestra Ansible module are exposed, under vars.outputs.

How it works

When outputsMode: EXPLICIT is set, the AnsibleCLI task passes KESTRA_OUTPUTS_MODE=explicit to the kestra_logger callback plugin. The callback then:

  1. Collects outputs only from kestra module calls — kestra: outputs: { key: value } in your playbook.
  2. Merges all declared outputs into a single flat dict (last write wins across hosts and module calls).
  3. Replaces each host’s full result payload in playbooks with {"changed": <bool>}. Failures and unreachable hosts also retain msg.
  4. Preserves task status (ok, failed, skipped, unreachable) so you can still inspect play health.

The kestra module is bundled with the plugin and injected automatically — no extra installation is needed.

Enable EXPLICIT mode

Add outputsMode: EXPLICIT to your AnsibleCLI task:

id: deploy
namespace: company.infra
tasks:
- id: run_playbook
type: io.kestra.plugin.ansible.cli.AnsibleCLI
outputsMode: EXPLICIT
containerImage: cytopia/ansible:latest-tools
inputFiles:
hosts: |
[webservers]
web-01
web-02
playbook.yml: |
- hosts: webservers
tasks:
- name: Gather app version
shell: cat /opt/app/VERSION
register: version_output
- name: Declare output
kestra:
outputs:
app_version: "{{ version_output.stdout }}"
commands:
- ansible-playbook -i hosts playbook.yml

Declare outputs with the kestra module

Use kestra: outputs: { key: value } anywhere in your playbook tasks. The module name is kestra (also resolvable as ansible.legacy.kestra).

- name: Declare database connection info
kestra:
outputs:
db_host: "{{ hostvars[inventory_hostname]['db_host'] }}"
schema_version: "{{ schema_version }}"

The module always sets changed: false. It is safe to run in check mode.

Avoid using loop

Do not call the kestra module inside an Ansible loop:. Ansible aggregates loop results under a results key, which the callback cannot parse for output collection. A warning is emitted and declared outputs are silently skipped for that task.

# WRONG — outputs will not be collected
- name: Declare per-item outputs
kestra:
outputs:
item: "{{ item }}"
loop: [a, b, c]
# CORRECT — call the module once with all values
- name: Declare outputs
kestra:
outputs:
items: [a, b, c]

Access outputs downstream

Declared outputs are available under outputs.<task_id>.vars.outputs:

- id: next_task
type: io.kestra.plugin.core.log.Log
message: "{{ outputs.run_playbook.vars.outputs.app_version }}"

Where run_playbook is the id of your AnsibleCLI task and app_version is the key you declared.

Output structure: ALL vs EXPLICIT

Both modes share the same vars.outputs shape — a flat map of keys declared via kestra: outputs: (empty if none were declared). The difference is what each host keeps in playbooks, where hosts is a list of {host, status, result} entries.

ALL mode (default): each host’s result is the Ansible result payload. With the default resultsStorage: STORE, playbooks holds a light result (changed, failed, skipped, unreachable, rc, msg), and the full result, including stdout, is in the file at resultsUri.

{
"vars": {
"outputs": {
"app_version": "1.4.2"
}
},
"playbooks": [
{
"plays": [
{
"tasks": [
{
"name": "Gather app version",
"hosts": [
{ "host": "web-01", "status": "ok", "result": { "changed": true, "rc": 0 } }
]
}
]
}
]
}
]
}

EXPLICIT mode: each host’s result is replaced with {"changed": <bool>} (plus msg for failed or unreachable hosts). vars.outputs still contains only the declared kestra module values.

{
"vars": {
"outputs": {
"app_version": "1.4.2"
}
},
"playbooks": [
{
"plays": [
{
"tasks": [
{
"name": "Gather app version",
"hosts": [
{ "host": "web-01", "status": "ok", "result": { "changed": true } }
]
}
]
}
]
}
]
}

By default, full per-host results are offloaded to a file at resultsUri (resultsStorage: STORE), and playbooks keeps a light result per host. Set resultsStorage: INLINE to keep the full results in playbooks, for example when a downstream task templates stdout. See resultsStorage and resultsUri for details and size limits.

resultsStorage and resultsUri

By default (resultsStorage: STORE), AnsibleCLI writes the full per-host Ansible results to a file in internal storage, available at outputs.<task_id>.resultsUri. playbooks keeps a light result per host: changed, failed, skipped, unreachable, rc, msg (cut to 1 KB) and, for failed or unreachable hosts, the last 1 KB of stderr. The output of debug tasks is kept inline. Read the full results downstream with {{ read(outputs.<task_id>.resultsUri) }}.

Set resultsStorage: INLINE to keep full results inside playbooks directly. Use this when a downstream task templates stdout or other fields that are not in the light result. Inline results count toward maxOutputsSize (default 10 MB), so large playbooks with many hosts can hit that limit.

tasks:
- id: run_playbook
type: io.kestra.plugin.ansible.cli.AnsibleCLI
resultsStorage: INLINE
outputsMode: EXPLICIT
...

Multi-host output merging

When multiple hosts call kestra: outputs: with the same key, the last write wins. If hosts run in parallel, which host writes last is non-deterministic. Use one of these patterns for predictable results:

Run the declaration on one host only:

- name: Declare run-level output
kestra:
outputs:
cluster_leader: "{{ inventory_hostname }}"
run_once: true
delegate_to: localhost

Key by hostname for per-host values:

- name: Declare per-host version
kestra:
outputs:
"version_{{ inventory_hostname }}": "{{ app_version }}"

Avoiding expression conflicts

Ansible uses {{ variable }} for Jinja2 expressions, and so does Kestra’s Pebble templating engine. When a playbook is defined inline in a Kestra flow (for example, in inputFiles or as a flow variable), Kestra evaluates its own {{ }} expressions before passing the file to Ansible — which breaks Ansible’s expressions.

Store playbooks that contain Ansible Jinja expressions in variables: to prevent Kestra from evaluating their {{ }} expressions:

id: deploy
namespace: company.infra
variables:
playbook: |
- hosts: webservers
tasks:
- name: Gather version
shell: cat /opt/app/VERSION
register: result
- name: Declare output
kestra:
outputs:
version: "{{ result.stdout }}"
tasks:
- id: run_playbook
type: io.kestra.plugin.ansible.cli.AnsibleCLI
outputsMode: EXPLICIT
containerImage: cytopia/ansible:latest-tools
inputFiles:
hosts: |
[webservers]
web-01
playbook.yml: "{{ vars.playbook }}"
commands:
- ansible-playbook -i hosts playbook.yml

The vars.playbook reference is resolved by Kestra, returning the raw playbook string. Ansible then evaluates {{ result.stdout }} as expected.

Custom ansibleConfig

If you supply a custom ansibleConfig, you must manually include the library path so Ansible can find the bundled kestra module. Without it, kestra: outputs: tasks will fail with a module-not-found error.

tasks:
- id: run_playbook
type: io.kestra.plugin.ansible.cli.AnsibleCLI
outputsMode: EXPLICIT
ansibleConfig: |
[defaults]
callback_plugins = ./callback_plugins
callbacks_enabled = kestra_logger
stdout_callback = ansible.builtin.null
library = ./library
commands:
- ansible-playbook -i hosts playbook.yml

Upgrading from plugin-ansible v1.x

Plugin-ansible v2.0.0 changed the output shape. Flows that read AnsibleCLI outputs downstream will need updating.

What changedOld expressionNew expression
vars.playbooks removedoutputs.<id>.vars.playbooks[...]outputs.<id>.playbooks[...]
vars.outputs in ALL mode no longer holds raw host resultsoutputs.<id>.vars.outputs[0].hostoutputs.<id>.playbooks[0].plays[0].tasks[0].hosts[0].host (payload under .result, or read resultsUri). vars.outputs now holds the kestra module values in both modes
Full per-host results offloaded by defaultoutputs.<id>.playbooks[...].hosts[0].result.stdout{{ read(outputs.<id>.resultsUri) }} or set resultsStorage: INLINE
stdout_lines / stderr_lines removedoutputs.<id>.playbooks[...].hosts[0].result.stdout_linesSplit result.stdout on newlines downstream

maxOutputsSize now measures the whole serialized task output rather than just the playbooks payload. If tasks start failing with an oversized output error, raise maxOutputsSize, switch to resultsStorage: STORE, or use outputsMode: EXPLICIT.

What EXPLICIT mode does not protect

outputsMode: EXPLICIT controls what is stripped from each host’s result in playbooks. It does not affect:

  • Ansible verbose logging (-v, -vv, etc.) — verbose output still appears in task logs.
  • log_path in ansible.cfg — if set, Ansible writes its full log to that path.
  • Kestra task logs — stdout from shell commands and module output may appear in execution logs.

If your playbooks handle highly sensitive data, use no_log: true on individual tasks to suppress their output from Ansible’s own logging, and avoid enabling verbose mode.

Was this page helpful?