Ansible Register Variables and Magic Variables Explained

Learn Ansible register variables for task output (stdout, stderr, rc) and magic variables such as inventory_hostname, groups, hostvars, and ansible_play_hosts, with loop results and when conditions.

Published

Updated

Read time 11 min read

Reviewed byDeepak Prasad

Ansible register variables and magic variables with stdout, hostvars, and inventory_hostname on Rocky Linux

Some tasks return data you need in the next step—a command exit code, a line of stdout, or whether a probe file exists. Other values already exist before you write a variable file: the current host name, group membership, or another host’s port from inventory. Ansible splits those into register variables (task output you capture) and magic variables (runtime context Ansible provides).

This guide shows how register works, which keys to read in the result hash, how loop output differs from a single task, and how magic variables such as inventory_hostname, groups, group_names, hostvars, and ansible_play_hosts fit into playbooks. It assumes you know Ansible variables, YAML syntax, and playbook structure. For full when syntax see conditionals; for loop basics see Ansible loop; for fact gathering see Ansible facts.

Tested on: Rocky Linux 10.2 (Red Quartz); kernel 6.12.0-211.16.1.el10_2.0.1.x86_64; ansible-core 2.16.16.

NOTE
This chapter is part of the GoLinuxCloud Ansible tutorial (RHCE EX294). Follow along from ~/ansible-project, inventory group lab, and playbooks in playbooks/. Use your own host names and paths if yours differ.

What are Register Variables and Magic Variables in Ansible?

Register variables hold the return value of one task. You choose the name with register: on that task and read fields such as result.stdout or result.rc later in the play.

Magic variables are internal names Ansible sets for you—current host, group lists, play host lists, and the hostvars dictionary. You do not define them in group_vars; Ansible overrides them automatically. The special variables documentation lists the full set.


Register Variables vs Magic Variables

Feature Register variables Magic variables
Created by You, with register on a task Ansible internally
Scope Result of one task (or loop) Play, inventory, and runtime context
Common use Read task output Access host, group, and play context
Examples result.stdout, result.rc inventory_hostname, groups, hostvars
Can user define? Yes No—Ansible overrides them

What is register in Ansible?

register is a task keyword. It tells Ansible to save the module’s return dictionary into a variable name you pick:

yaml
- name: Run hostname command
  ansible.builtin.command: hostname
  register: host_out

After this task, host_out exists for that host during the current playbook run. It is stored in memory and is not saved for future playbook executions. Any module can be registered—not only command and shell.

Registered variables are host-scoped: host_out on web1 is a separate dict from host_out on web2. Each managed host stores its own registration for the play—you cannot read another host's registered result without delegating or gathering facts another way.

When the registering task uses loop:, loop results land in a .results list—there is no top-level registered.stdout. Read registered.results[0].stdout for the first item, or loop over registered.results and test each element's failed, rc, or stdout.


Basic register Example

Project layout for the demos:

text
ansible-project/
├── ansible.cfg
├── inventory/
│   └── hosts.yml
└── playbooks/
    └── register-basic.yml

Inventory:

yaml
# inventory/hosts.yml
all:
  children:
    web:
      hosts:
        web1:
          ansible_host: 127.0.0.1
          ansible_connection: local
          web_port: 8080
        web2:
          ansible_host: 127.0.0.1
          ansible_connection: local
          web_port: 8081
    db:
      hosts:
        db1:
          ansible_host: 127.0.0.1
          ansible_connection: local
          db_port: 5432

Playbook:

yaml
# playbooks/register-basic.yml
---
- name: Basic register demo
  hosts: web1
  gather_facts: false
  tasks:
    - name: Run hostname command
      ansible.builtin.command: hostname
      register: host_out
      changed_when: false

    - name: Show full registered structure
      ansible.builtin.debug:
        var: host_out

The debug task above is the fastest way to learn field names—the debug Ansible playbooks guide expands that workflow with verbosity and check mode.

Run from the project root:

bash
cd ~/ansible-project
ansible-playbook playbooks/register-basic.yml

Sample output:

output
ok: [web1] => {
    "host_out": {
        "changed": false,
        "cmd": [
            "hostname"
        ],
        "failed": false,
        "rc": 0,
        "stderr": "",
        "stdout": "rocky1",
        "stdout_lines": [
            "rocky1"
        ]
    }
}

Your hostname string differs; the shape of the hash is what matters.


Understand Registered Variable Output

Registered results share a common skeleton. Module-specific keys may appear on top.

stdout

Standard output from command-style modules—one string:

yaml
msg: "{{ host_out.stdout }}"

stderr

Error or warning text from the command. Empty on success for many commands:

yaml
when: host_out.stderr != ""

rc

Return code (exit status). 0 usually means success for commands:

yaml
when: host_out.rc == 0

changed

Whether Ansible considers the task a change. Read-only commands often set changed_when: false so changed stays false.

failed

Whether the task failed. Combine with failed_when: false when a non-zero rc is expected.

stdout_lines

stdout split into a list—handy when you need line 2 or want to loop lines:

yaml
first_line: "{{ host_out.stdout_lines[0] }}"

Use register with command and shell Tasks

command and shell are the modules beginners register most often. Prefer ansible.builtin.command when you do not need pipes or shell builtins; use shell only when the shell is required—see command vs shell vs raw.

yaml
- name: Capture disk line with shell
  ansible.builtin.shell: df -h / | tail -1
  register: disk_line
  changed_when: false

- name: Use stdout in a message
  ansible.builtin.debug:
    msg: "Root filesystem: {{ disk_line.stdout }}"

Dedicated modules (dnf, stat, lineinfile) return richer keys than stdout/rc. Register them the same way and read the module documentation for field names.


Use Registered Variables in debug Tasks

While learning, dump the full hash once, then narrow to the keys you need:

yaml
- name: Inspect everything
  ansible.builtin.debug:
    var: host_out

- name: Inspect one field
  ansible.builtin.debug:
    msg:
      stdout: "{{ host_out.stdout }}"
      rc: "{{ host_out.rc }}"

debug: var= is the fastest way to discover unexpected keys from a new module.


Use Registered Variables in when Conditions

Use registered fields in when without wrapping the expression in {{ }}—the value is already a Jinja expression:

yaml
- name: Check if /etc/hosts exists
  ansible.builtin.command: test -f /etc/hosts
  register: hosts_file
  changed_when: false
  failed_when: false

- name: Report when file exists
  ansible.builtin.debug:
    msg: "hosts file present, rc={{ hosts_file.rc }}"
  when: hosts_file.rc == 0

Sample output:

output
ok: [web1] => {
    "msg": "hosts file present, rc=0"
}

More operators and patterns live in the conditionals guide—here we only wire when to registered rc and stdout.


Use register with failed_when and changed_when

Register first, then customize status on the same task:

yaml
- name: Check for missing file
  ansible.builtin.command: test -f /tmp/not-there-ansible-demo
  register: missing_file
  changed_when: false
  failed_when: false

- name: Treat rc 1 as expected
  ansible.builtin.debug:
    msg: "missing file check rc={{ missing_file.rc }}"
  when: missing_file.rc == 1

failed_when: false keeps rc: 1 from stopping the play when “file missing” is normal. changed_when: false marks the probe as read-only. Idempotency covers more changed_when patterns with register.


Register Variables with Loops

When the task uses loop:, the registered variable contains a results list—one element per iteration. There is no top-level stdout on the registered name itself.

Understand results list

yaml
- name: Run command per item
  ansible.builtin.command: "echo {{ item }}"
  loop:
    - alpha
    - beta
  register: echo_loop
  changed_when: false

- name: Show results list length
  ansible.builtin.debug:
    msg: "loop count={{ echo_loop.results | length }}"

Sample output:

output
ok: [web1] => {
    "msg": "loop count=2"
}

Access item output

Each list entry mirrors a normal registered result plus item (the loop value):

yaml
- name: Show first item stdout
  ansible.builtin.debug:
    msg: "item0 stdout={{ echo_loop.results[0].stdout }}"

Sample output:

output
ok: [web1] => {
    "msg": "item0 stdout=alpha"
}

Check failed loop items

Loop over registered.results and test each element:

yaml
- name: List failed loop items if any
  ansible.builtin.debug:
    msg: "failed item={{ item.item }} rc={{ item.rc }}"
  loop: "{{ echo_loop.results }}"
  when: item.failed | default(false)

When every item succeeds, Ansible skips this task. For loop syntax and loop_control, see Ansible loop.


What are Magic Variables in Ansible?

Magic variables (Ansible also calls them special variables) describe runtime context: which host is running, which groups exist, which hosts remain in the play, and variables for other hosts. They are available during playbook execution, but the exact values depend on the current host, inventory, active play hosts, and whether facts have been gathered. You cannot replace them by defining the same name in inventory—Ansible wins.


Common Ansible Magic Variables

inventory_hostname

The inventory name Ansible uses in logs and file names—web1, not necessarily the SSH target:

jinja2
{{ inventory_hostname }}

groups

Dictionary mapping group name to a list of host names in that group:

jinja2
{{ groups['web'] }}

Ad hoc check:

bash
ansible web1 -m ansible.builtin.debug -a "var=groups"

Sample output:

output
web1 | SUCCESS => {
    "groups": {
        "all": [
            "web1",
            "web2",
            "db1"
        ],
        "db": [
            "db1"
        ],
        "web": [
            "web1",
            "web2"
        ]
    }
}

group_names

List of groups the current host belongs to (excluding all):

jinja2
{{ group_names }}

On web1 this is ['web'].

hostvars

Dictionary of per-host variables for every host in inventory. Access another host’s inventory vars:

jinja2
{{ hostvars['web2']['web_port'] }}

Facts for other hosts appear in hostvars only after fact gathering ran for those hosts (default at play start, or after setup / gather_facts). Before gathering, you still see inventory-defined keys.

ansible_play_hosts

List of hosts still active in the current play (not yet failed or unreachable):

jinja2
{{ ansible_play_hosts }}

With hosts: web, both web1 and web2 appear while the play runs.

ansible_play_batch

Hosts in the current serial batch. With serial: 1, the batch is one host at a time:

jinja2
{{ ansible_play_batch }}

On the first batch for web1, ansible_play_batch is ['web1'] while ansible_play_hosts still lists all play hosts.

Example task:

yaml
- name: Show magic variables on this host
  ansible.builtin.debug:
    msg:
      inventory_hostname: "{{ inventory_hostname }}"
      ansible_host: "{{ ansible_host }}"
      group_names: "{{ group_names }}"
      play_hosts: "{{ ansible_play_hosts }}"
      play_batch: "{{ ansible_play_batch }}"
      web2_port: "{{ hostvars['web2']['web_port'] }}"

inventory_hostname vs ansible_host

Name Meaning Example
inventory_hostname Name Ansible uses from inventory web1, rocky2
ansible_host Connection address when different 127.0.0.1, 192.168.56.109

Use inventory_hostname in messages, host_vars file names, and hostvars['web1'] lookups. Use ansible_host when you need the address SSH connects to. More detail: inventory files.


Use groups to Access Inventory Groups

Test membership or build host lists dynamically:

yaml
- name: Run only when web group has two members
  ansible.builtin.debug:
    msg: "web members={{ groups['web'] | length }}"
  when: groups['web'] | length >= 2

groups['all'] includes every host. Prefer explicit group names over hard-coding host lists in playbooks.


Use hostvars to Access Variables from Other Hosts

Typical pattern: read a port or VIP from another tier during a rolling update:

yaml
msg: "Peer web2 listens on {{ hostvars['web2']['web_port'] }}"

Facts on web2 (for example ansible_distribution) need gather_facts: true (default) or an earlier setup against web2. Inventory-only keys such as web_port work immediately.


Use ansible_play_hosts and ansible_play_batch

Use ansible_play_hosts when a task must know how many hosts are still in the play:

yaml
- name: Show remaining play hosts
  ansible.builtin.debug:
    msg: "still running={{ ansible_play_hosts }}"

Combine serial with ansible_play_batch for rolling restarts—each batch sees only its slice. Targeting which hosts enter the play is inventory and pattern design; see group_vars and patterns rather than repeating it here.


Register Variables vs Facts vs Magic Variables

Kind Source You define? Typical examples
Register Prior task on this host Yes (register:) probe.rc, api.json
Facts setup / gather_facts No (discovered) ansible_os_family, ansible_memtotal_mb
Magic Ansible runtime No inventory_hostname, hostvars, groups
Inventory vars group_vars, host_vars Yes web_port, app_env

Registered variables are task output stored for a host during the current playbook run. Facts are discovered system data—see Ansible facts. Magic variables expose inventory and play machinery. Variable precedence explains which layer wins when names collide; this article does not repeat that order.


Common Mistakes with register and Magic Variables

Mistake Symptom Fix
registered.stdout after a loop Undefined or wrong value Use registered.results[n].stdout or loop results
Confuse inventory_hostname with IP Wrong host_vars filename or hostvars key File is web1.yml; IP goes in ansible_host
Read peer facts before gathering Key missing in hostvars['other'] Run gather_facts: true or setup on that host first
Define groups in group_vars Unpredictable overrides Pick a different variable name
Skip failed_when: false on probes Play stops on expected rc Register and test rc in when
Register every task “just in case” Noisy vars, harder debugging Register only when a later task needs the output

In a small Ansible project:

  1. Register command or stat probes when a later task branches on rc or file content.
  2. Use debug: var= while learning, then narrow to stdout or module-specific keys.
  3. Use inventory_hostname in messages and hostvars['web2'] for peer inventory data.
  4. Rely on default fact gathering before reading another host’s facts through hostvars.
  5. Avoid storing permanent configuration in registered names—they last for the play, not the repo.

References


Summary

register captures one task’s return value into a variable you name—read stdout, stderr, rc, changed, failed, and stdout_lines for command-style results. Loops store results as a list. Magic variables such as inventory_hostname, groups, group_names, hostvars, ansible_play_hosts, and ansible_play_batch are provided by Ansible and describe hosts, groups, and play context. Use register for task output; use magic variables for inventory and play awareness; use facts for discovered system data.


Frequently Asked Questions

1. What is the difference between register and magic variables in Ansible?

register saves one task result into a variable you choose. Magic variables such as inventory_hostname and hostvars are created automatically by Ansible for every play and host.

2. What keys does a registered command result contain?

Common keys include stdout, stderr, rc, changed, failed, and stdout_lines. Module tasks may add module-specific keys on top of that structure.

3. How do I read loop output after register?

A registered loop stores a results list. Use registered.results[0].stdout for the first item, or loop registered.results and test item.failed per element.

4. What is inventory_hostname vs ansible_host?

inventory_hostname is the name Ansible uses from inventory—web1 in our demo. ansible_host is the connection address when it differs, such as 127.0.0.1 or a host-only IP.

5. Can I set magic variables in group_vars?

No. Ansible overrides magic variables internally. Define your own names such as app_environment instead of trying to replace inventory_hostname or hostvars.

6. When are facts available inside hostvars?

After fact gathering runs for a host—by default at play start, or when you run setup or gather_facts. Before that, hostvars may show inventory data only.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive …