The Logic Gap: Why Skipped Tasks Break Playbooks
Imagine you're halfway through a production deployment when your playbook stops abruptly. This isn't caused by a server crash or a network glitch. Instead, it's a logical edge case. You have a task that saves its result to a variable, but because that task was skipped, the next step fails while trying to read data that doesn't exist.
This error typically surfaces when you combine a register statement with a when conditional. If the condition is false, the task is bypassed. Consequently, the registered variable lacks standard keys like rc (Return Code), stdout, or stderr.
The Problem Scenario
- name: Check if custom service is running
command: systemctl is-active my-service
register: service_check
when: check_service_health | default(false)
ignore_errors: yes
- name: Restart service if it failed the check
service:
name: my-service
state: restarted
when: service_check.rc != 0
If check_service_health is false, the first task is skipped. Ansible still initializes the service_check variable to keep track of the state, but it only contains basic metadata:
{
"skipped": true,
"changed": false
}
When the second task tries to access service_check.rc, it hits a wall. Since there is no rc key in a skipped result, Jinja2 throws a fit:
The conditional check 'service_check.rc != 0' failed. The error was: 'dict object' has no attribute 'rc'
Why Ansible Behaves This Way
Ansible's register keyword is persistent. It creates the variable even if the module never executes. This is intentional; it allows you to check variable.skipped in later tasks. However, the specific return values of a module—like the 0 or 1 return code from a shell command—only exist if the command actually ran.
In the world of Jinja2, accessing a missing dictionary key is a fatal error. Your playbook stops immediately to prevent unpredictable behavior.
Solution 1: The 'default' Filter (Quick Fix)
To resolve this quickly, use the Jinja2 default filter. This provides a fallback value so the comparison has something to work with, even if the task was skipped.
- name: Restart service if it failed the check
service:
name: my-service
state: restarted
when: (service_check.rc | default(0)) != 0
By using default(0), we assume a "success" state if the check never ran. Since 0 != 0 is false, the restart task is safely bypassed without crashing.
Solution 2: Robust Logic with Skip Checks
Relying on defaults can sometimes mask actual failures. A more professional approach is to explicitly check if the task was skipped before evaluating its result.
Explicit Skip Check
- name: Restart service if it failed the check
service:
name: my-service
state: restarted
when:
- service_check is not skipped
- service_check.rc != 0
Ansible evaluates when lists sequentially. If service_check is not skipped is false, Ansible stops right there. It won't even try to look for the rc attribute on the second line, effectively dodging the error.
Using the 'is defined' Test
If you want maximum safety, verify that the attribute exists at all:
- name: Restart service if it failed the check
service:
name: my-service
state: restarted
when: service_check.rc is defined and service_check.rc != 0
Verification: Testing the Fix
Don't wait for a production run to see if your logic holds up. You can simulate the skip easily.
- Trigger the skip: Run your playbook with a variable override:
-e "check_service_health=false". - Inspect the JSON: Add a
debugtask to see exactly what Ansible sees:
- name: Debug service check result
debug:
var: service_check
- Run with Verbosity: Use the
-vflag. You should see the task output asskippingrather thanfatal. This confirms your conditional logic is now handling the missing attributes gracefully.
Key Takeaways
- Variable Persistence: Registered variables are always created if the task is reached, regardless of whether it executes.
- Partial Content: A skipped task's dictionary is nearly empty, usually containing only
skipped: true. - Safe Referencing: Always use
| default()oris not skippedwhen pulling data from tasks that have their ownwhenconditions. - Lazy Evaluation: In a list of conditions, Ansible stops at the first
false. Put your existence checks first.

