SSD Nodes Learn Hosting plans →
Guides Matt ConnorBy Matt Connor

Show command output in an Ansible playbook

At default verbosity a command task shows only ok or changed. Run with -v or use register and debug to see its output, and keep secrets out of logs.

Verified Every command ran end-to-end on a fresh Ubuntu 24.04 server, October 24, 2026.

How to show command output in Ansible

To show command output in an Ansible playbook, save the task result with register, then print it with the debug module. At the default verbosity, a command or shell task prints only a status line such as ok or changed. This happens because the default output plugin adds the full task result only when you run with -v or higher. Ansible captured the output. It just did not display it. Running with -v also shows it, without any extra task.

This guide builds one small playbook that runs on the machine you type on, with one task per idea. You will learn the difference between var: and msg:. You will see what stdout, stdout_lines, stderr and rc each hold. You will see how -v changes the screen, and why a pipe works under shell but not under command. If you have never written a playbook, start with your first Ansible playbook on a VPS and come back.

Run every command yourself and read your own screen. Ansible's output changes between releases. So this guide explains where each value lives and what it means. It does not copy one machine's terminal.

Install ansible-core on Ubuntu 24.04

On Ubuntu 24.04 the archive carries ansible-core 2.16.3. That is older than the current upstream release, but it has every feature this guide uses.

sudo apt update
sudo apt install -y ansible-core
ansible --version

The first line of ansible --version shows the core version. Write it down, because two sections below depend on it.

This install brings in more than ansible-core. On a fresh Ubuntu 24.04 system it also adds a large set of community collections, including community.general 8.3.0. That version matters for the YAML section below.

Make a working directory for the examples:

mkdir -p ~/show-output
cd ~/show-output

The playbook: register and debug

Save this as ~/show-output/output.yml.

- name: Show command output
  hosts: localhost
  connection: local
  gather_facts: false
  tasks:
    - name: Run a command that writes to stdout and stderr
      ansible.builtin.shell: echo first; echo second; echo oops >&2
      register: demo
      changed_when: false

    - name: Show the whole registered result
      ansible.builtin.debug:
        var: demo

    - name: Show only the output lines
      ansible.builtin.debug:
        var: demo.stdout_lines

    - name: Show a sentence built from the result
      ansible.builtin.debug:
        msg: "rc was {{ demo.rc }}, stderr was {{ demo.stderr }}"

The settings at the top keep the example small. hosts: localhost with connection: local runs every task on this machine without SSH, so you do not need an inventory file. When you move to real servers, you list them in the Ansible inventory file. gather_facts: false skips fact collection, because this playbook does not use facts. The page on what gather_facts collects and when to turn it off explains the trade-off.

Run it:

ansible-playbook output.yml

The first task reports only its status. The three debug tasks print what the first task captured. That is the whole technique. register saves the result, and debug shows it.

register is a task keyword. The Ansible docs describe it as the "name of variable that will contain task status and module return data". Any later task in the run can read that variable for the same host.

changed_when: false tells Ansible that this command changes nothing. Without it, a shell or command task reports changed on every run. Ansible cannot know what an arbitrary command did, so it assumes a change.

What stdout, stdout_lines, stderr and rc hold

The command and shell modules return the four fields you will use most.

  • stdout is the normal output of the command, as one string. Separate lines are joined by newline characters.
  • stdout_lines is the same output as a list of strings, one item per line. Ansible always provides it when it returns stdout.
  • stderr is the error output, as one string. A command can print to stderr and still succeed.
  • rc is the return code. The command module docs say it plainly: 0 means success.

Pick the field for the job. Use stdout_lines when a person will read the output, because each line prints as its own item. Use stdout in a when: test or a template, where one string is easier to match. Use rc when the question is "did it work". Read stderr when a command succeeds but still warns you about something.

In the first debug task, var: demo prints the whole result dictionary. Read it once on your own screen. You will find these four fields there, plus several more, such as the command that ran and its start and end times.

debug var versus debug msg

The debug module takes one of two options. The docs say they are mutually exclusive, so use one per task.

var: takes a variable name. The docs note that this option "already runs in Jinja2 context and has an implicit {{ }} wrapping". So you write the name bare:

- name: Right
  ansible.builtin.debug:
    var: demo.stdout_lines

Do not wrap the name in braces. The docs say to add Jinja2 delimiters to var: only if you want double interpolation. With braces, Ansible first replaces the expression with its value. It then treats that value as a new name to look up, which is almost never what you want.

msg: takes a string, and here you do write the braces. Use it to put a value inside a sentence, or to join several values on one line. If you leave out msg: completely, the module prints its default message, Hello world!.

A short rule: var: for one value you want to inspect, msg: for text you want a person to read.

How -v changes what ansible-playbook shows

You do not always need a debug task. Ansible's variables guide says it directly: "To see the values for a particular task, run your playbook with -v." Run the same playbook with -v:

ansible-playbook output.yml -v

Now the first task prints its result too. Before the default output plugin adds the full result to a status line, it checks whether the run is verbose. Any -v counts as verbose. That check is why a bare command task looks silent. The result was there all along.

Each extra v raises the level. The ansible-playbook docs say the built-in plugins evaluate up to -vvvvvv. They suggest -vvv as a reasonable starting level for troubleshooting, and -vvvv for connection problems. Compare -v and -vv on your own run to see what each level adds.

ansible-playbook output.yml -vv

-v is a good habit while you write a playbook. It is a poor long-term answer, because it prints every task result. The one output you care about gets lost among the rest.

Make a debug task show only at -vv

The debug module has a verbosity option. It is an integer and its default is 0. The task runs only when the playbook runs at that -v level or higher. The docs give the example that verbosity: 3 runs only at -vvv or above.

Add this task to the end of output.yml:

    - name: Show stderr only when asked
      ansible.builtin.debug:
        var: demo.stderr
        verbosity: 2

Run the playbook twice:

ansible-playbook output.yml
ansible-playbook output.yml -vv

In the first run, this task does not print demo.stderr, because the run is below level 2. In the second run, it does. This is how you keep diagnostic tasks in a playbook without filling every normal run with them.

command versus shell when the output comes through a pipe

The command module does not start a shell. Its docs say the command "will not be processed through the shell, so operations like" *, <, >, |, ; and & "will not work". To see why that matters, add these three tasks:

    - name: Pipe under command
      ansible.builtin.command: echo hello | tr a-z A-Z
      register: piped_command
      changed_when: false

    - name: Pipe under shell
      ansible.builtin.shell: echo hello | tr a-z A-Z
      register: piped_shell
      changed_when: false

    - name: Compare the two
      ansible.builtin.debug:
        msg: "command gave '{{ piped_command.stdout }}', shell gave '{{ piped_shell.stdout }}'"

Run the playbook and read the last task. Under command, no shell reads the |. Ansible splits the line into words and starts echo with all of them as arguments. So |, tr, a-z and A-Z reach echo as plain text, and echo prints them. The task does not fail. It returns the wrong text with rc 0, which is why this mistake is easy to miss.

Under shell, the line goes to a shell. The shell builds a real pipe from echo into tr.

The shell docs still say that "if you want to execute a command securely and predictably, it may be better to use" the command module. A shell also expands variables and globs. That becomes a problem when part of the command comes from a variable. A fair rule: use command by default, and switch to shell only when you need a pipe or a redirect.

One more point about pipes. In a shell, the return code of a pipeline is the return code of its last command. If the first command fails and the last one succeeds, rc is 0 and the task reports success. In bash you can change that with set -o pipefail. To use it, name bash as the shell with the executable option:

    - name: Fail if any part of the pipe fails
      ansible.builtin.shell: set -o pipefail && cat /etc/hostname | tr a-z A-Z
      args:
        executable: /bin/bash
      register: strict_pipe
      changed_when: false

Show the output of a command that failed

A command that returns a non-zero rc makes its task fail. By default the play then stops on that host, so your debug task never runs. register still saves the result of the failed task. Let the play continue, then print it:

    - name: Run a command that fails
      ansible.builtin.command: ls /does-not-exist
      register: broken
      ignore_errors: true
      changed_when: false

    - name: Show why it failed
      ansible.builtin.debug:
        msg: "rc={{ broken.rc }} stderr={{ broken.stderr }}"

ls cannot find the path. It writes the reason to stderr and exits with a non-zero code, and the debug task shows both. Remove ignore_errors: true once you have fixed the problem, because it also hides real failures.

Can Ansible print command output as YAML?

The default output plugin, called default, has an option named result_format. Its choices are json, which is the default, and yaml. It was added in ansible-core 2.13, so the 2.16.3 package on Ubuntu 24.04 has it. Set it in an ansible.cfg file in your working directory:

[defaults]
callback_result_format = yaml

Or set it for one run with an environment variable:

ANSIBLE_CALLBACK_RESULT_FORMAT=yaml ansible-playbook output.yml -v

To confirm that Ansible read your ansible.cfg, list only the settings that differ from the defaults:

ansible-config dump --only-changed

The docs give two warnings. First, these formats "do not cause the callback to emit valid JSON or YAML formats", so do not feed this output to a parser. Second, with yaml the pretty_results option defaults to true, and the docs say YAML may change module responses to make them easier to read. For an exact view of a result, set result_format back to json.

Does stdout_callback = yaml still work?

Many older guides teach this line instead:

[defaults]
stdout_callback = yaml

That line does not set the option above. It replaces the whole output plugin, and only one output plugin can be active at a time. In the ansible-core source, the short name yaml redirects to community.general.yaml, a plugin from the community.general collection. The community.general docs say this plugin "has been removed in version 12.0.0", and that result_format=yaml in the default plugin replaces it.

So the answer depends on which community.general version is installed on your control machine. Check:

ansible-galaxy collection list

The apt install above brings in community.general 8.3.0. That is older than 12.0.0, so on a stock Ubuntu 24.04 system the yaml plugin is still part of the installed collection. Set the line, run the playbook, and read what your own screen prints. If your control machine has no community.general, or has 12.0.0 or newer, the redirect points at nothing. When ansible-core cannot load the configured output plugin, its source code raises an error and the run stops. The exact wording differs between ansible-core versions, so read it on your own screen. Either way the old line works only with community.general versions older than 12.0.0, because 12.0.0 is the release that removed the plugin. Delete the stdout_callback = yaml line and use callback_result_format = yaml.

Hide output that contains secrets with no_log

Everything above makes output easier to see. Some output must never be seen. A task that reads a token or sets a password puts that value in its result, and -v or a debug task will show it. Anything that collects your terminal output, such as a CI (continuous integration) log, then keeps a copy.

The no_log task keyword is a boolean that, in the docs' words, "controls information disclosure". Set it on the task that handles the secret:

    - name: Read the API token
      ansible.builtin.command: cat /etc/myapp/token
      register: api_token
      changed_when: false
      no_log: true

Run the play with -v and compare this task's line with the others. Then treat the registered variable as a secret too. Any later task that prints api_token needs no_log: true as well. The logging docs also warn that no_log "does not affect debugging output", which is why they say not to debug playbooks in production. The better fix is to keep the secret out of the command in the first place. Store it with Ansible Vault to encrypt secrets in your playbooks and pass it only to the task that needs it.

To see what a playbook would change before it runs, use Ansible check mode and --diff. A command task with no creates or removes does not run in check mode, so its registered stdout is an empty string.

FAQ

Why does my Ansible command task not show any output?

The default output plugin adds the full task result to the status line only when the run is verbose, which means -v or higher. Ansible captures the output either way. Add register: result to the task and a debug task with var: result.stdout_lines after it, or run ansible-playbook with -v.

What is the difference between stdout and stdout_lines in Ansible?

stdout is the command's normal output as one string, with lines joined by newline characters. stdout_lines is the same output as a list, one string per line. Use stdout_lines when a person reads the result in a debug task, and stdout when you test or template the value.

Should I use debug var or debug msg?

Use var: to inspect one variable, and write its name without {{ }}, because the option already wraps it in Jinja2. Use msg: to print a sentence, and put each value inside {{ }}. You cannot use both options in one task.

Why does a pipe not work in the Ansible command module?

The command module does not run a shell, so |, >, ; and & reach the program as plain arguments. echo hello | tr a-z A-Z under command prints the literal words and still returns rc 0. Use ansible.builtin.shell when you need a pipe or a redirect.

How do I get YAML output from ansible-playbook?

Set callback_result_format = yaml under [defaults] in ansible.cfg, or set the ANSIBLE_CALLBACK_RESULT_FORMAT=yaml environment variable. It needs ansible-core 2.13 or later. The older stdout_callback = yaml depends on the community.general.yaml plugin, which was removed in community.general 12.0.0.

Was this article useful?