ansible-idempotency
Techniques for writing truly idempotent Ansible tasks using changed_when, failed_when, and check-before-create patterns to eliminate spurious "changed" reports.
What this skill does
# Ansible Idempotency Patterns
Techniques for ensuring Ansible tasks are truly idempotent - producing the same result
whether run once or multiple times.
## Core Directives
### changed_when
Controls when Ansible reports a task as "changed". Critical for `command` and `shell` modules
which always report changed by default.
```yaml
- name: Check if service exists
ansible.builtin.command: systemctl status myservice
register: service_check
changed_when: false # Read-only operation, never changes anything
```
### failed_when
Controls when Ansible considers a task failed. Allows graceful handling of expected errors.
```yaml
- name: Check resource existence
ansible.builtin.command: check-resource {{ resource_id }}
register: check_result
failed_when: false # Don't fail, we'll check the result ourselves
```
### register
Captures task output for use in `changed_when` and `failed_when` expressions.
```yaml
- name: Run command
ansible.builtin.command: some-command
register: cmd_result
# Now cmd_result.rc, cmd_result.stdout, cmd_result.stderr are available
```
## Pattern 1: Detect Actual Changes
Make commands report "changed" only when something actually changed:
```yaml
- name: Create Proxmox API token
ansible.builtin.command: >
pveum user token add {{ username }}@pam {{ token_name }}
register: token_result
changed_when: "'already exists' not in token_result.stderr"
failed_when:
- token_result.rc != 0
- "'already exists' not in token_result.stderr"
no_log: true
```
**Key pattern**: Detect specific output that indicates no change occurred.
## Pattern 2: Check Before Create
Check if a resource exists before creating it:
```yaml
- name: Check if VM template exists
ansible.builtin.shell: |
set -o pipefail
qm list | awk '{print $1}' | grep -q "^{{ template_id }}$"
args:
executable: /bin/bash
register: template_exists
changed_when: false # Checking doesn't change anything
failed_when: false # Not finding it isn't a failure
- name: Create VM template
ansible.builtin.command: >
qm create {{ template_id }}
--name {{ template_name }}
--memory 2048
when: template_exists.rc != 0 # Only create if doesn't exist
register: create_result
changed_when: create_result.rc == 0
```
## Pattern 3: Verify After Create
Confirm resource creation succeeded:
```yaml
- name: Create VM
ansible.builtin.command: >
qm create {{ vmid }} --name {{ vm_name }}
register: create_result
changed_when: true
- name: Verify VM was created
ansible.builtin.shell: |
set -o pipefail
qm list | grep "{{ vmid }}"
args:
executable: /bin/bash
register: verify_result
changed_when: false
failed_when: verify_result.rc != 0
```
## Pattern 4: Conditional Change Detection
Use output content to determine if change occurred:
```yaml
- name: Update cluster configuration
ansible.builtin.command: update-config --apply
register: update_result
changed_when: "'Configuration updated' in update_result.stdout"
failed_when: "'Error' in update_result.stderr"
```
### Common Patterns
| Output Indicator | changed_when Expression |
|-----------------|------------------------|
| "already exists" | `"'already exists' not in result.stderr"` |
| "no changes" | `"'no changes' not in result.stdout"` |
| "created" | `"'created' in result.stdout"` |
| "updated" | `"'updated' in result.stdout"` |
| Exit code 0 = created | `result.rc == 0` |
## Pattern 5: Multiple Failure Conditions
Allow specific "failures" that are actually expected:
```yaml
- name: Run database migration
ansible.builtin.command: /usr/bin/migrate-database
register: migrate_result
failed_when:
- migrate_result.rc != 0
- "'already applied' not in migrate_result.stdout"
- "'no pending migrations' not in migrate_result.stdout"
changed_when: "'applied' in migrate_result.stdout and 'already' not in migrate_result.stdout"
```
## Pattern 6: Read-Only Operations
Mark read-only operations as never changed:
```yaml
# Checking status
- name: Get cluster status
ansible.builtin.command: pvecm status
register: cluster_status
changed_when: false
failed_when: false
# Gathering information
- name: List available images
ansible.builtin.command: qm list
register: vm_list
changed_when: false
# Verification checks
- name: Verify service is running
ansible.builtin.command: systemctl is-active nginx
register: nginx_status
changed_when: false
failed_when: false
```
## Pattern 7: Retry Until Success
Use `until` for operations that may need retries:
```yaml
- name: Wait for service to be ready
ansible.builtin.uri:
url: http://localhost:8080/health
status_code: 200
register: health_check
until: health_check.status == 200
retries: 30
delay: 10
# Total wait: up to 5 minutes
```
With command:
```yaml
- name: Wait for VM to get IP address
ansible.builtin.command: qm agent {{ vmid }} network-get-interfaces
register: vm_network
until: vm_network.rc == 0
retries: 12
delay: 5
changed_when: false
```
## Pattern 8: Set Facts for State
Use facts to track state across tasks:
```yaml
- name: Check existing cluster status
ansible.builtin.command: pvecm status
register: cluster_status
failed_when: false
changed_when: false
- name: Set cluster facts
ansible.builtin.set_fact:
is_cluster_member: "{{ cluster_status.rc == 0 }}"
in_target_cluster: "{{ cluster_name in cluster_status.stdout }}"
- name: Create cluster
ansible.builtin.command: pvecm create {{ cluster_name }}
when: not in_target_cluster
register: cluster_create
changed_when: cluster_create.rc == 0
```
## Anti-Patterns to Avoid
### Always Changed
```yaml
# BAD - Always shows changed
- name: Check status
ansible.builtin.command: systemctl status app
# GOOD
- name: Check status
ansible.builtin.command: systemctl status app
register: status_check
changed_when: false
failed_when: false
```
### Silent Failure Suppression
```yaml
# BAD - Hides all errors
- name: Critical operation
ansible.builtin.command: important-command
failed_when: false
# GOOD - Only allow expected "errors"
- name: Critical operation
ansible.builtin.command: important-command
register: result
failed_when:
- result.rc != 0
- "'expected condition' not in result.stderr"
```
### No Output Capture
```yaml
# BAD - Can't check results
- name: Run command
ansible.builtin.command: create-resource
# GOOD
- name: Run command
ansible.builtin.command: create-resource
register: result
changed_when: "'created' in result.stdout"
```
## Shell Script Requirements
Use strict error handling in shell scripts:
```yaml
- name: Run pipeline
ansible.builtin.shell: |
set -euo pipefail
cat data.txt | grep pattern | sort | uniq
args:
executable: /bin/bash
register: pipeline_result
changed_when: false
```
### Why set -euo pipefail?
| Flag | Purpose |
|------|---------|
| `-e` | Exit on any command failure |
| `-u` | Error on undefined variables |
| `-o pipefail` | Catch errors in pipelines |
## Testing Idempotency
Verify playbooks are idempotent by running twice:
```bash
# First run - may show changes
uv run ansible-playbook playbooks/setup.yml
# Second run - should show 0 changes
uv run ansible-playbook playbooks/setup.yml
# If second run shows changes, playbook is NOT idempotent
```
## Common changed_when Expressions
```yaml
# Never changed (read-only)
changed_when: false
# Always changed (one-time operations)
changed_when: true
# Based on output content
changed_when: "'created' in result.stdout"
changed_when: "'already exists' not in result.stderr"
changed_when: "'updated' in result.stdout"
# Based on return code
changed_when: result.rc == 0
changed_when: result.rc != 1
# Complex conditions
changed_when:
- result.rc == 0
- "'no changes' not in result.stdout"
```
## Utility Script
Use the idempotency checker to analyze playbooks for common issues:
```bash
# Check a siRelated in Writing & Docs
jax-development
IncludedUse this skill when the user is writing, debugging, profiling, refactoring, reviewing, benchmarking, parallelising, exporting, or explaining JAX code, or when they mention JAX, jax.numpy, jit, grad, value_and_grad, vmap, scan, lax, random keys, pytrees, jax.Array, sharding, Mesh, PartitionSpec, NamedSharding, pmap, shard_map, Pallas, XLA, StableHLO, checkify, profiler, or the JAX repo. It helps turn NumPy or PyTorch-style code into pure functional JAX, fix tracer/control-flow/shape/PRNG bugs, remove recompiles and host-device syncs, choose transforms and sharding strategies, inspect jaxpr/lowering/IR, and benchmark compiled code correctly.
nature-article-writer
IncludedDrafts, rewrites, diagnostically critiques, and style-calibrates primary research manuscripts for Nature and Nature Portfolio journals. Use when the user wants a Nature-style title, summary paragraph or abstract, introduction, results, discussion, methods, figure legends, presubmission enquiry, cover letter, reviewer response, or when a scientific draft sounds generic, jargon-heavy, structurally weak, or AI-ish and needs precise, broad-reader-friendly prose without inventing data, analyses, or references. Best for primary research articles and letters rather than reviews or press releases unless explicitly adapting one.
deckrd
IncludedDocument-driven framework that derives requirements, specifications, implementation plans, and executable tasks from goals through structured AI dialogue. Use when user says "write requirements", "create spec", "plan implementation", "derive tasks", "structure this feature", "break down into tasks", or "document this module". Also use for reverse engineering existing code into docs (/deckrd rev). Do NOT use for direct code writing — use /deckrd-coder after tasks are generated. Do NOT use when the user only wants to run or fix existing code without planning.
clinical-decision-support
IncludedGenerate professional clinical decision support (CDS) documents for pharmaceutical and clinical research settings, including patient cohort analyses (biomarker-stratified with outcomes) and treatment recommendation reports (evidence-based guidelines with decision algorithms). Supports GRADE evidence grading, statistical analysis (hazard ratios, survival curves, waterfall plots), biomarker integration, and regulatory compliance. Outputs publication-ready LaTeX/PDF format optimized for drug development, clinical research, and evidence synthesis.
handling-sf-data
IncludedSalesforce data operations with 130-point scoring. Use this skill to create, update, delete, bulk import/export, generate test data, and clean up org records using sf CLI and anonymous Apex. TRIGGER when: user creates test data, performs bulk import/export, uses sf data CLI commands, needs data factory patterns for Apex tests, or needs to seed/clean records in a Salesforce org. DO NOT TRIGGER when: SOQL query writing only (use querying-soql), Apex test execution (use running-apex-tests), or metadata deployment (use deploying-metadata).
accelint-ac-to-playwright
IncludedConvert and validate acceptance criteria for Playwright test automation. Use when user asks to (1) review/evaluate/check if AC are ready for automation, (2) assess if AC can be converted as-is, (3) validate AC quality for Playwright, (4) turn AC into tests, (5) generate tests from acceptance criteria, (6) convert .md bullets or .feature Gherkin files to Playwright specs, (7) create test automation from requirements. Handles both bullet-style markdown and Gherkin syntax with JSON test plan generation and validation.