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.
~/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:
- name: Run hostname command
ansible.builtin.command: hostname
register: host_outAfter 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:
ansible-project/
├── ansible.cfg
├── inventory/
│ └── hosts.yml
└── playbooks/
└── register-basic.ymlInventory:
# 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: 5432Playbook:
# 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_outThe 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:
cd ~/ansible-project
ansible-playbook playbooks/register-basic.ymlSample 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:
msg: "{{ host_out.stdout }}"stderr
Error or warning text from the command. Empty on success for many commands:
when: host_out.stderr != ""rc
Return code (exit status). 0 usually means success for commands:
when: host_out.rc == 0changed
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:
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.
- 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:
- 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:
- 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 == 0Sample 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:
- 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 == 1failed_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
- 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:
ok: [web1] => {
"msg": "loop count=2"
}Access item output
Each list entry mirrors a normal registered result plus item (the loop value):
- name: Show first item stdout
ansible.builtin.debug:
msg: "item0 stdout={{ echo_loop.results[0].stdout }}"Sample output:
ok: [web1] => {
"msg": "item0 stdout=alpha"
}Check failed loop items
Loop over registered.results and test each element:
- 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:
{{ inventory_hostname }}groups
Dictionary mapping group name to a list of host names in that group:
{{ groups['web'] }}Ad hoc check:
ansible web1 -m ansible.builtin.debug -a "var=groups"Sample 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):
{{ group_names }}On web1 this is ['web'].
hostvars
Dictionary of per-host variables for every host in inventory. Access another host’s inventory vars:
{{ 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):
{{ 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:
{{ 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:
- 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:
- name: Run only when web group has two members
ansible.builtin.debug:
msg: "web members={{ groups['web'] | length }}"
when: groups['web'] | length >= 2groups['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:
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:
- 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 |
Recommended Usage
In a small Ansible project:
- Register command or
statprobes when a later task branches onrcor file content. - Use
debug: var=while learning, then narrow tostdoutor module-specific keys. - Use
inventory_hostnamein messages andhostvars['web2']for peer inventory data. - Rely on default fact gathering before reading another host’s facts through
hostvars. - Avoid storing permanent configuration in registered names—they last for the play, not the repo.
References
- Ansible: Using variables — registered variables
- Ansible: Special variables — magic variables list
- Ansible: Controlling loop behavior
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.

